(三星难度)怎么拍摄的VR图像,全景图像拼接

作品简介

双鱼眼全景是我最开始本科入门cv的第一个项目,尽管后面研究生换成了大模型方向,它仍是我最喜爱的一个方向之一。最后,由于楼主去年毕业开始在互联网当社畜,没有精力再做360渲染软件了, 这个渲染是我后面初步尝试用pyopengl写的, 期待能给大家带来启迪。

import cv2
import numpy as np
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *

# 全局变量
window_width, window_height = 1080, 1080
panorama_image = None
texture_id = None
yaw, pitch = 0.0, 0.0 # 水平和垂直角度

# 加载全景图像
def load_panorama_image(image_path):
    global panorama_image
    panorama_image = cv2.imread(image_path)
    if panorama_image is None:
        print(f"Failed to load image from {image_path}")
        exit()

# 初始化 OpenGL
def init_opengl():
    glEnable(GL_DEPTH_TEST)
    glEnable(GL_TEXTURE_2D)
    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)

# 创建纹理
def create_texture():
    global texture_id
    height, width, _ = panorama_image.shape
    print(height, width)
    texture_id = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, texture_id)
    image = cv2.flip(panorama_image, 0) # 翻转图像
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 转换为 RGB 格式
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image)
    glGenerateMipmap(GL_TEXTURE_2D)

# 渲染全景球
def render_sphere(radius=1.0, sectors=180, stacks=180):
    for i in range(stacks):
        stack_angle1 = np.pi / 2 - i * np.pi / stacks
        stack_angle2 = np.pi / 2 - (i + 1) * np.pi / stacks
        xy1 = radius * np.cos(stack_angle1)
        xy2 = radius * np.cos(stack_angle2)
        z1 = radius * np.sin(stack_angle1)
        z2 = radius * np.sin(stack_angle2)
        
        glBegin(GL_QUAD_STRIP)
        for j in range(sectors + 1):
            sector_angle = j * 2 * np.pi / sectors
            x = np.cos(sector_angle)
            y = np.sin(sector_angle)
            
            u1 = j / sectors
            v1 = i / stacks
            u2 = j / sectors
            v2 = (i + 1) / stacks
            
            glTexCoord2f(u1, v1)
            glVertex3f(x * xy1, y * xy1, z1)
            glTexCoord2f(u2, v2)
            glVertex3f(x * xy2, y * xy2, z2)
        glEnd()

# 显示回调
def display():
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBindTexture(GL_TEXTURE_2D, texture_id)
    glPushMatrix()
    render_sphere()
    glPopMatrix()
    glutSwapBuffers()

# 窗口大小调整回调
def reshape(width, height):
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(110.0, width / height, 0.1, 100.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    update_camera()

# 键盘控制视角
def keyboard(key, x, y):
    global yaw, pitch
    if key == b'w':
        pitch += 5
    elif key == b's':
        pitch -= 5
    elif key == b'a':
        yaw += 5
    elif key == b'd':
        yaw -= 5
    elif key == b'q':
        exit()

    # 限制 pitch 角度在 -90 到 90 之间
    # pitch = max(min(pitch, 90), -90)
    # 更新视角
    update_camera()

# 更新摄像机视角
def update_camera():
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    # 计算观察点坐标
    look_at_x = np.sin(np.radians(yaw))
    look_at_y =  np.cos(np.radians(pitch)) * np.cos(np.radians(yaw))
    look_at_z = np.sin(np.radians(pitch))

    # 摄像机位置固定在球心 (0, 0, 0),观察点由 yaw 和 pitch 决定
    gluLookAt(0.0, 0.0, 0.0, look_at_x, look_at_y, look_at_z, 0.0, 1.0, 0.0)

# 主函数
def main(image_path):
    load_panorama_image(image_path)
    glutInit()
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
    glutInitWindowSize(window_width, window_height)
    glutCreateWindow(b"Panorama Viewer with Fixed Camera")
    glutDisplayFunc(display)
    glutReshapeFunc(reshape)
    glutIdleFunc(display)
    glutKeyboardFunc(keyboard)
    init_opengl()
    create_texture()
    glutMainLoop()

# 调用主函数并传入全景图像路径
if __name__ == "__main__":
    main("result.jpg")  # 请替换为你的全景图路径



Done【2024/10/25】附件[eloft.zip]:

  1. 新增2024sota 图像特征点算法:EfficientLoFTR
  2. 新增鱼眼矫正不同展开算法: Link

Update【2023/12/13】:新增GUI可以调试效果,详情分享见: https://blog.csdn.net/hard_level/article/details/134958120

双鱼眼拼接算法网上几乎没有完整的项目,博主花了大量的时间搭建目前较强的pipeline。创作不易,谢谢大家的支持

代码见付费区(之前购买也可下载)


1. 简介

大家都知道VR,能够看到360度全景图像,即每帧图像记录了360度视角的成像。而工业界是怎样拍摄这样的图像的呢?目前博主了解的主要有两种做法:

  • A.使用多个正常摄像头,每个摄像头拍摄固定角度的图像,然后再拼接
  • B.使用两个视角(FOV)>=180度的鱼眼摄像头,再拼接如下图


我们将该任务明确输入输出:


输入:给定两张鱼眼图像拍摄的图像
输出:一张拼接好的矩形映射全景图(后续你可以根据这张全景图去做各种趣味剪辑)

2.我的解决方案

2.1 算法模块拆解

我先google网上的做法:发现类似的功能: https://moonagic.com/dualfisheye-to-equirectangular/

但是效果不是特别好。结合自己的经验,我将pipeline分为了三部分:

2.1.1 鱼眼相机的畸变矫正

目的: 将鱼眼图像展开为矩形图需要进行图像校正或投影变换。鱼眼图像通常采用一种特殊的透视投影,因此需要将其转换为常规的等距或直角投影以获得矩形图像。这个过程通常涉及到计算每个像素在新图像中的位置,然后将像素从鱼眼图像复制到新图像中的对应位置。

方法: 我们的方法分为两步。第一步: 我们将2维成像的鱼眼原始图像投影在3D球面上。第二步:我们采用球面透射投影, 我们首先定义了球面上的点的极坐标,然后计算了这些点的笛卡尔坐并使用球面透射投影的数学公式计算了投影点的坐标。


2.1.2 特征点检测与匹配

目的: 我们将两个鱼眼图像转化为两张矩形图像,他们在边缘位置并没有对齐。在这里我们假设两张图像的边缘位置存在overlap, 然后我们需要在overlap中找到两张图的对齐变换关系,使得他们在overlap区间图像能够完全重合。

方法: 我们的方法仍然分为两步。第一步:我们将检测出overlap区间中的特征点来联系两个图像。特征点通常是图像中具有独特结构或纹理的位置,例如角点、边缘、斑点等。第二步:我们将使用特征点各自的特征描述子来进行匹配。我们支持下面三种方法:

ORB/NN

SIFT/NN

SuperPoint/SuperGlue

2.1.3 矫正对齐与拼接

目的: 将上面的两个图像合并成一个无缝的整体的过程。

方法: 我们的方法仍然涉及到两个主要步骤:图像矫正与图像拼接。第一步:图像矫正是利用上面获取的特征点匹配的信息来获取一个变换矩阵(单应性矩阵)。这个变换可以是平移、旋转、缩放、仿射变换或透视变换等来使两个图像通过在相同的坐标系统下对齐。第二步: 绝大多数情况下,overlap区间的图像不是完全对齐的, 如果使用阈值将两张图的overlap直接叠加,会使得拼接处有重影。我们使用了最佳缝合线的策略


2.2 环境搭建

python>= 3.6

matplotlib>=3.1.3

numpy>=1.18.1

opencv-python

opencv-contrib-python

torch>=1.3.0 (superpoint)

2.3 效果调试

3 里面有我的online方式

如果大家有安装,调试和优化方向的问题,可以问我,我将免费解答。






创作时间: