Bootstrap

透视投影(Perspective projection)与等距圆柱投影(Equirectangular projection)

一、透视投影

1.方法概述

        Perspective projection(透视投影)是一种模拟人眼观察三维空间物体时的视觉效果的投影方法。它通过模拟观察者从一个特定视点观察三维场景的方式来创建二维图像。在透视投影中,远处的物体看起来比近处的物体小,这与我们的日常视觉经验相符。

1.1关键特点

  1. 单点透视:最基本的透视投影形式,所有消失点汇聚在一个点上,这个点称为消失点。这种投影适用于观察者直接面对的场景。

  2. 双点透视:当场景有一个角落或边缘时使用,消失点分布在两个不同的点上。

  3. 三点透视:用于非常倾斜或垂直的视角,消失点分布在三个不同的点上。

  4. 消失点:在透视投影中,平行线在无限远处相交的点称为消失点。这些点帮助确定物体在空间中的相对位置和大小。

  5. 透视网格:艺术家和设计师经常使用透视网格来帮助绘制透视图像。网格提供了一个框架,用于确定物体在透视空间中的位置。

  6. 比例和尺寸:在透视投影中,物体的大小与其与观察者的距离成反比。物体越远,看起来越小。

  7. 深度感:透视投影通过大小变化和重叠来创造深度感,使图像看起来更立体。

1.2 相关应用

  1. 艺术和设计中的应用:透视投影在绘画、建筑绘图、游戏设计和电影制作等领域中非常重要,因为它帮助创造真实感和空间感。

  2. 数学模型:在计算机图形学中,透视投影可以通过数学模型实现,其中3D坐标被转换为2D屏幕坐标,同时考虑视角和消失点。

  3. 透视投影矩阵:在3D图形编程中,透视投影矩阵用于将3D场景转换为2D视图,这是通过一系列线性变换实现的。

        透视投影是理解和创建三维空间中物体如何在二维平面上呈现的基础,它对于任何涉及空间表现的领域都是至关重要的。

2.数学推导

2.1视锥体到视平面的投影

在透视投影中,我们首先需要将视锥体中的顶点投影到视平面上。这可以通过相似三角形的性质来实现。对于三维空间中的一条直线,其参数方程可以表示为:

其中(x0​,y0​,z0​) 为直线上的一点,t 为参数。根据相似三角形原理,我们可以得到:

其中 f 为相机焦距。当 t→∞ 时,我们可以得到灭点的坐标:

这表明灭点只取决于直线的方向,而与直线上具体的点无关。

2.2 视锥体到规则观察体的映射

接下来,我们需要将视锥体映射到规则观察体(CVV)中进行裁剪。这涉及到将顶点坐标转换为齐次坐标,并构造出一个变换矩阵来完成映射。透视投影矩阵的数学推导过程可以表示为:

        其中,N 是近平面距离,f 是远平面距离,R,L,T,B 分别是视口的右边界、左边界、上边界和下边界值。

2.3 透视除法

在将点映射到NDC(标准设备坐标系)之后,我们需要进行透视除法,即将 x′, y′, z′ 坐标除以 w′ 来得到最终的屏幕坐标:

2.4 视口变换和比例恢复

最后,我们需要将NDC坐标映射到屏幕坐标系中,并进行比例恢复。这涉及到将NDC坐标转换为栅格坐标系统,其中单位是像素。这个过程可以通过以下公式实现:

其中,PixelWidthPixelWidth 和 PixelHeightPixelHeight 分别是屏幕的像素宽度和高度。

二、等距圆柱投影

1.方法概述

        Equirectangular-perspective projection(等距柱状透视投影)是一种将球面(如地球)映射到平面矩形上的方法。这种投影方式保持了垂直和水平方向的一致间距,因此它在全景成像和虚拟现实应用中非常流行,因为它允许轻松地将图像拼接成广角视图。

1.1关键特点

  1. 等距柱状投影:也被称为地理或等距圆柱投影,它将纬度和经度线均匀地投影到平面上。
  2. 形状和面积失真:这种投影可能会扭曲形状和面积,尤其是在极地附近,但它提供了一种直观的全景图像布局,通过允许从球面到矩形坐标的简单映射。
  3. 虚拟现实应用:在虚拟现实应用中,等距柱状图像可以显示在360度环境中,为用户创造沉浸式体验。
  4. 全景图像创建:等距柱状格式通常与通过特殊相机或设置创建的球面全景图一起使用。
  5. 渲染技术:在视频游戏和模拟中,等距柱状投影的渲染技术对于创建无缝环境以增强用户参与度至关重要。

等距柱状投影的主要特点是其2:1的宽高比,能够覆盖360°的水平视场和180°的垂直视场,同时保持点之间的角关系,但会在极地附近扭曲形状。这种投影方式在全景摄影中被广泛用于创建完整的360度水平和180度垂直视图。

1.2相关应用

        等距柱状透视投影(Equirectangular projection)在多个领域有着广泛的应用,以下是一些主要的应用场景:

        全景图像和虚拟现实(VR):等距柱状透视投影是全景图像和虚拟现实中常用的一种投影方式。它能够将360度的全景图像映射到2D平面上,为用户提供沉浸式的体验。在VR应用中,这种投影方式可以让用户感受到身临其境的效果,例如通过Three.js实现全景图VR,将全景图贴到球体上,实现虚拟环境的模拟。

        全球数据集:等距柱状投影因其简单的地理关系,成为了全球栅格数据集的标准,例如Celestia和NASA World Wind。这种投影方式使得图像像素在地图上的位置与其对应的地理位置之间的关系变得简单直接。

        地图制作:尽管等距柱状投影在导航或地籍测绘中使用较少,因为它引入了畸变,但它在主题地图制作中找到了主要应用。这种投影方式能够将球面上的经纬线投影到圆柱面上,然后展开成平面,适用于地图的制作和展示。

        3D全景场景生成:在3D全景场景生成中,等距柱状投影被用来将3D球体映射到2D平面,这对于生成详细、一致且合理的全景图至关重要。通过这种投影,可以合成360°×90°视角的全景图,并逐步扩展至360°×180°视角的全景图。

2.数学推导

2.1 正向投影

正向投影是将球面坐标(经度和纬度)转换为平面坐标的过程。假设我们有一个点在球面上,其球面坐标为(φ, λ),其中φ是纬度,λ是经度。等距柱状透视投影的正向投影公式如下:

  • 经度(x): x=λ
  • 纬度(y): y=ϕ

这里,λ被直接映射为x坐标,而φ被直接映射为y坐标。这种投影方式保持了经度和纬度的等距特性,即在球面上等距的经线和纬线在投影后仍然保持等距。

2.2 反向投影

反向投影是将平面坐标转换回球面坐标的过程。给定平面上的点(x, y),我们可以通过以下公式计算其对应的球面坐标(φ, λ):

  • 经度(λ): λ=x
  • 纬度(φ): ϕ=y

这里,x坐标直接对应于经度,y坐标直接对应于纬度。

2.3 归一化与反归一化

在实际应用中,我们经常需要将平面坐标归一化到[-1, 1]的范围内,以适应不同的显示和处理需求。归一化的公式如下:

反归一化的公式则为:

这些公式允许我们将平面坐标在[-1, 1]范围内归一化,并在需要时将其反归一化回原始的经度和纬度值。

2.4 笛卡尔坐标系转化

在某些应用中,我们需要将球面坐标转换为笛卡尔坐标。对于单位球面上的点,转换公式如下:

        X=sin(ϕ)cos(λ)

        Y=sin(ϕ)sin(λ)

        Z=cos(ϕ)

这些公式将球面坐标(φ, λ)转换为笛卡尔坐标系中的点(X, Y, Z)。

三、相关代码详解

1.透视投影

1.1 数学推导

        透视投影的数学推导可以通过多种编程语言实现,这里我提供一个简单的Python代码示例,用于演示如何将三维空间中的点通过透视投影转换到二维平面上。这个例子中,我们将使用一个简单的透视投影矩阵来实现这一转换。

import numpy as np

def perspective_projection(point, fov, aspect_ratio, near, far):
    """
    对三维点进行透视投影。
    
    参数:
    point -- 三维空间中的点 (x, y, z)
    fov -- 视场角,以度为单位
    aspect_ratio -- 宽高比
    near -- 近裁剪面
    far -- 远裁剪面
    
    返回:
    二维平面上的投影点 (x', y', w')
    """
    # 将视场角从度转换为弧度
    fov_rad = np.radians(fov)
    
    # 计算透视投影矩阵
    f = 1 / np.tan(fov_rad / 2)
    projection_matrix = np.array([
        [f / aspect_ratio, 0, 0, 0],
        [0, f, 0, 0],
        [0, 0, (far + near) / (near - far), (2 * far * near) / (near - far)],
        [0, 0, -1, 0]
    ])
    
    # 将点转换为齐次坐标
    point_homogeneous = np.array([point[0], point[1], point[2], 1])
    
    # 应用透视投影矩阵
    projected_point = np.dot(projection_matrix, point_homogeneous)
    
    # 归一化w分量
    projected_point /= projected_point[3]
    
    return projected_point[:2]

# 示例使用
point_3d = (1, 2, 3)  # 三维空间中的点
fov_degrees = 60  # 视场角
aspect_ratio = 16 / 9  # 宽高比
near_plane = 0.1  # 近裁剪面
far_plane = 100  # 远裁剪面

projected_point = perspective_projection(point_3d, fov_degrees, aspect_ratio, near_plane, far_plane)
print("Projected Point:", projected_point)

        函数定义perspective_projection 函数接受一个三维点和透视投影的参数(视场角、宽高比、近裁剪面和远裁剪面)。视场角转换:将视场角从度转换为弧度,因为三角函数在Python中使用弧度。透视投影矩阵:构建一个4x4的透视投影矩阵,这个矩阵将用于将三维点转换到二维平面。点转换:将三维点转换为齐次坐标(添加一个1作为w分量)。应用投影矩阵:将透视投影矩阵应用于齐次坐标点。归一化:将结果除以w分量以归一化坐标。返回结果:返回归一化后的二维坐标。

1.2 实际应用

        在实际的项目中,透视投影通常用于图形渲染和3D图形处理。以下是一个使用Python和OpenGL库PyOpenGL实现的简单示例,这个示例创建了一个窗口,并在其中渲染了一个简单的3D立方体,使用透视投影来模拟相机视角。

import glfw
from OpenGL.GL import *
from OpenGL.GL.shaders import compileProgram, compileShader

# 顶点着色器
vertex_shader = """
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
"""

# 片段着色器
fragment_shader = """
#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
"""

def create_shaders():
    vertex = compileShader(vertex_shader, GL_VERTEX_SHADER)
    fragment = compileShader(fragment_shader, GL_FRAGMENT_SHADER)
    return compileProgram(vertex, fragment)

def main():
    if not glfw.init():
        return

    window = glfw.create_window(800, 600, "Perspective Projection Example", None, None)
    
    if not window:
        glfw.terminate()
        return

    glfw.make_context_current(window)

    shader = create_shaders()
    glUseProgram(shader)

    # 设置视口
    glViewport(0, 0, 800, 600)

    # 设置投影矩阵
    projection = glm.perspective(glm.radians(45.0), 800/600, 0.1, 100.0)
    glUniformMatrix4fv(glGetUniformLocation(shader, "projection"), 1, GL_FALSE, glm.value_ptr(projection))

    # 设置视图矩阵
    view = glm.lookAt(glm.vec3(4, 3, 3), glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
    glUniformMatrix4fv(glGetUniformLocation(shader, "view"), 1, GL_FALSE, glm.value_ptr(view))

    vertices = [
        -0.5, -0.5, 0.0, 1.0,
         0.5, -0.5, 0.0, 1.0,
         0.0,  0.5, 0.0, 1.0
    ]

    VBO = glGenBuffers(1)
    VAO = glGenVertexArrays(1)
    glBindVertexArray(VAO)

    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, (GLfloat * len(vertices))(*vertices), GL_STATIC_DRAW)

    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)
    glEnableVertexAttribArray(0)

    while not glfw.window_should_close(window):
        glClearColor(0.2, 0.3, 0.3, 1.0)
        glClear(GL_COLOR_BUFFER_BIT)

        # 更新模型矩阵
        model = glm.rotate(glm.mat4(1.0), glfw.get_time(), glm.vec3(0, 1, 0))
        glUniformMatrix4fv(glGetUniformLocation(shader, "model"), 1, GL_FALSE, glm.value_ptr(model))

        glDrawArrays(GL_TRIANGLES, 0, 3)

        glfw.swap_buffers(window)
        glfw.poll_events()

    glDeleteVertexArrays(1, [VAO])
    glDeleteBuffers(1, [VBO])

    glfw.terminate()

import glm
main()

        初始化GLFW和OpenGL:设置窗口和OpenGL上下文。着色器编译:编译顶点着色器和片段着色器,并链接成程序。设置视口和投影矩阵:设置视口大小,并创建透视投影矩阵。设置视图矩阵:创建视图矩阵,模拟相机视角。顶点数据和缓冲区:定义顶点数据,并创建VBO和VAO。渲染循环:在渲染循环中,清除颜色缓冲区,更新模型矩阵,并绘制三角形。

2.等距圆柱投影

2.1 数学推导

等距圆柱投影(Equirectangular Projection)的数学推导可以通过编程语言实现,这里提供一个简单的Python代码示例,用于演示如何将球面坐标(纬度和经度)转换为等距圆柱投影的平面坐标。

import numpy as np

def spherical_to_equirectangular(latitude, longitude, scale=1.0):
    """
    将球面坐标(纬度和经度)转换为等距圆柱投影的平面坐标。
    
    参数:
    latitude -- 纬度,范围:-π/2 到 π/2
    longitude -- 经度,范围:-π 到 π
    scale -- 缩放因子,默认为1.0
    
    返回:
    平面坐标 (x, y)
    """
    # 将纬度和经度转换为弧度
    lat_rad = np.radians(latitude)
    lon_rad = np.radians(longitude)
    
    # 计算等距圆柱投影的平面坐标
    x = scale * lon_rad
    y = scale * np.sin(lat_rad)
    
    return x, y

def equirectangular_to_spherical(x, y, scale=1.0):
    """
    将等距圆柱投影的平面坐标转换回球面坐标(纬度和经度)。
    
    参数:
    x -- 平面坐标的x分量
    y -- 平面坐标的y分量
    scale -- 缩放因子,默认为1.0
    
    返回:
    球面坐标 (latitude, longitude)
    """
    # 计算纬度和经度
    longitude = np.degrees(x / scale)
    latitude = np.degrees(np.arcsin(y / scale))
    
    return latitude, longitude

# 示例使用
latitude = -30  # 纬度
longitude = 60  # 经度

# 球面坐标到等距圆柱投影坐标
x, y = spherical_to_equirectangular(latitude, longitude)
print(f"Equirectangular Coordinates: ({x}, {y})")

# 等距圆柱投影坐标到球面坐标
lat, lon = equirectangular_to_spherical(x, y)
print(f"Spherical Coordinates: ({lat}, {lon})")

(1)球面坐标到等距圆柱投影坐标spherical_to_equirectangular 函数接受纬度和经度作为输入,并将其转换为等距圆柱投影的平面坐标。纬度(latitude)和经度(longitude)首先被转换为弧度。然后,使用公式 x = scale * lon_rad 和 y = scale * np.sin(lat_rad) 计算平面坐标。

(2)等距圆柱投影坐标到球面坐标equirectangular_to_spherical 函数接受平面坐标作为输入,并将其转换回球面坐标。使用公式 longitude = np.degrees(x / scale) 和 latitude = np.degrees(np.arcsin(y / scale)) 计算纬度和经度。

2.2 实际应用

        使用Python和OpenGL库来实现一个简单的全景图像查看器,它将展示如何将球面坐标转换为等距圆柱投影坐标,并在屏幕上显示。

import glfw
from OpenGL.GL import *
from OpenGL.GL.shaders import compileProgram, compileShader
import numpy as np

# 顶点着色器
vertex_shader = """
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 projection;
uniform mat4 view;

void main()
{
    gl_Position = projection * view * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}
"""

# 片段着色器
fragment_shader = """
#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D texture1;

void main()
{
    FragColor = texture(texture1, TexCoord);
}
"""

def create_shaders():
    vertex = compileShader(vertex_shader, GL_VERTEX_SHADER)
    fragment = compileShader(fragment_shader, GL_FRAGMENT_SHADER)
    return compileProgram(vertex, fragment)

def main():
    if not glfw.init():
        return

    window = glfw.create_window(800, 600, "Equirectangular Projection Example", None, None)
    if not window:
        glfw.terminate()
        return

    glfw.make_context_current(window)

    shader = create_shaders()
    glUseProgram(shader)

    # 设置视口
    glViewport(0, 0, 800, 600)

    # 设置投影矩阵
    projection = np.array([
        [1.0, 0.0, 0.0, 0.0],
        [0.0, 1.0, 0.0, 0.0],
        [0.0, 0.0, 1.0, 0.0],
        [0.0, 0.0, 0.0, 1.0]
    ])  # 单位矩阵作为投影矩阵,因为我们处理的是2D图像

    glUniformMatrix4fv(glGetUniformLocation(shader, "projection"), 1, GL_FALSE, projection)
    glUniformMatrix4fv(glGetUniformLocation(shader, "view"), 1, GL_FALSE, projection)  # 同样使用单位矩阵

    # 加载纹理
    texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, texture)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    # 加载全景图像(这里需要一个全景图像文件路径)
    with open("path_to_your_equirectangular_image.jpg", "rb") as f:
        bin = f.read()
        img = bytearray(bin)
        glBindTexture(GL_TEXTURE_2D, texture)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 400, 0, GL_RGB, GL_UNSIGNED_BYTE, img)
        glGenerateMipmap(GL_TEXTURE_2D)

    # 顶点数据
    vertices = [
        -1.0, -1.0, 0.0, 0.0, 0.0,
         1.0, -1.0, 0.0, 1.0, 0.0,
         1.0,  1.0, 0.0, 1.0, 1.0,
        -1.0,  1.0, 0.0, 0.0, 1.0
    ]

    VBO = glGenBuffers(1)
    VAO = glGenVertexArrays(1)
    glBindVertexArray(VAO)

    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, (GLfloat * len(vertices))(*vertices), GL_STATIC_DRAW)

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), ctypes.c_void_p(12))
    glEnableVertexAttribArray(1)

    while not glfw.window_should_close(window):
        glClearColor(0.1, 0.1, 0.1, 1.0)
        glClear(GL_COLOR_BUFFER_BIT)

        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, texture)
        glUniform1i(glGetUniformLocation(shader, "texture1"), 0)

        glDrawArrays(GL_TRIANGLE_FAN, 0, 4)

        glfw.swap_buffers(window)
        glfw.poll_events()

    glDeleteVertexArrays(1, [VAO])
    glDeleteBuffers(1, [VBO])

    glfw.terminate()

import ctypes
main()

        初始化GLFW和OpenGL:设置窗口和OpenGL上下文。着色器编译:编译顶点着色器和片段着色器,并链接成程序。设置视口和投影矩阵:设置视口大小,并使用单位矩阵作为投影矩阵。加载纹理:加载全景图像作为纹理。顶点数据和缓冲区:定义顶点数据,并创建VBO和VAO。渲染循环:在渲染循环中,清除颜色缓冲区,绑定纹理,并绘制四边形。

;