将 2:1 equirectangular 全景图转换为立方体贴图

Posted

技术标签:

【中文标题】将 2:1 equirectangular 全景图转换为立方体贴图【英文标题】:Convert 2:1 equirectangular panorama to cube map 【发布时间】:2015-06-23 02:00:09 【问题描述】:

我目前正在为一个网站开发一个简单的 3D 全景查看器。出于移动性能原因,我使用 Three.js CSS 3 renderer。这需要一个立方体贴图,分成六个单独的图像。

我正在使用 Google Photo Sphere 或可创建 2:1 equirectangular 全景图的类似应用在 iPhone 上录制图像。然后,我使用此网站调整大小并将其转换为立方体贴图:http://gonchar.me/panorama/ (Flash)

最好自己进行转换,如果可能的话,可以在 Three.js 中即时进行,或者在 Photoshop 中进行。我找到了 Andrew Hazelden 的 Photoshop 动作,它们看起来很接近,但没有直接转换可用。有没有一种数学方法来转换这些,或者某种脚本可以做到这一点?如果可能的话,我想避免使用像 Blender 这样的 3D 应用程序。

也许这是一个长镜头,但我想我会问。我对 javascript 有很好的经验,但我对 Three.js 还是很陌生。我也对依赖WebGL 功能犹豫不决,因为它在移动设备上看起来要么很慢,要么有问题。支持也仍然参差不齐。

【问题讨论】:

在 javascript 中使用 CSS 或画布可以做到这一点。但是我不确定它是否与three.js 兼容。 ***.com/questions/8912917/… 我创建了一个 python 脚本来执行此操作github.com/seiferteric/cubemap 【参考方案1】:

如果你想在服务器端做,有很多选择。 ImageMagick 有一堆命令行工具,可以将你的图像分割成碎片。您可以将执行此操作的命令放入脚本中,并在每次有新图像时运行它。

很难说清楚程序中使用了什么算法。我们可以通过将方形网格输入程序来尝试对正在发生的事情进行逆向工程。我用过grid from Wikipedia:

这给出了:

这为我们提供了有关盒子是如何构造的线索。

想象一个上面有经纬线的球体,以及一个围绕它的立方体。现在从球体中心点投影会在立方体上产生一个扭曲的网格。

在数学上,取极坐标 r, θ, ø,对于球体 r=1, 0

x= r sin θ cos ø y= r sin θ sin ø z= r cos θ

将这些集中投影到立方体。首先我们按照纬度 -π/4

假设我们在第一边 -π/4

a sin θ cos ø = 1

所以

a = 1 / (sin θ cos ø)

投影点是

(1, tan ø, cot θ / cos ø)

如果 |婴儿床 θ / cos ø | 1 或tan θ

将这些放在 Python 中:

import sys
from PIL import Image
from math import pi,sin,cos,tan

def cot(angle):
    return 1/tan(angle)

# Project polar coordinates onto a surrounding cube
# assume ranges theta is [0,pi] with 0 the north poll, pi south poll
# phi is in range [0,2pi]
def projection(theta,phi):
        if theta<0.615:
            return projectTop(theta,phi)
        elif theta>2.527:
            return projectBottom(theta,phi)
        elif phi <= pi/4 or phi > 7*pi/4:
            return projectLeft(theta,phi)
        elif phi > pi/4 and phi <= 3*pi/4:
            return projectFront(theta,phi)
        elif phi > 3*pi/4 and phi <= 5*pi/4:
            return projectRight(theta,phi)
        elif phi > 5*pi/4 and phi <= 7*pi/4:
            return projectBack(theta,phi)

def projectLeft(theta,phi):
        x = 1
        y = tan(phi)
        z = cot(theta) / cos(phi)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Left",x,y,z)

def projectFront(theta,phi):
        x = tan(phi-pi/2)
        y = 1
        z = cot(theta) / cos(phi-pi/2)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Front",x,y,z)

def projectRight(theta,phi):
        x = -1
        y = tan(phi)
        z = -cot(theta) / cos(phi)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Right",x,-y,z)

def projectBack(theta,phi):
        x = tan(phi-3*pi/2)
        y = -1
        z = cot(theta) / cos(phi-3*pi/2)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Back",-x,y,z)

def projectTop(theta,phi):
        # (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,1)
        a = 1 / cos(theta)
        x = tan(theta) * cos(phi)
        y = tan(theta) * sin(phi)
        z = 1
        return ("Top",x,y,z)

def projectBottom(theta,phi):
        # (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,-1)
        a = -1 / cos(theta)
        x = -tan(theta) * cos(phi)
        y = -tan(theta) * sin(phi)
        z = -1
        return ("Bottom",x,y,z)

# Convert coords in cube to image coords
# coords is a tuple with the side and x,y,z coords
# edge is the length of an edge of the cube in pixels
def cubeToImg(coords,edge):
    if coords[0]=="Left":
        (x,y) = (int(edge*(coords[2]+1)/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Front":
        (x,y) = (int(edge*(coords[1]+3)/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Right":
        (x,y) = (int(edge*(5-coords[2])/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Back":
        (x,y) = (int(edge*(7-coords[1])/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Top":
        (x,y) = (int(edge*(3-coords[1])/2), int(edge*(1+coords[2])/2) )
    elif coords[0]=="Bottom":
        (x,y) = (int(edge*(3-coords[1])/2), int(edge*(5-coords[2])/2) )
    return (x,y)

# convert the in image to out image
def convert(imgIn,imgOut):
    inSize = imgIn.size
    outSize = imgOut.size
    inPix = imgIn.load()
    outPix = imgOut.load()
    edge = inSize[0]/4   # the length of each edge in pixels
    for i in xrange(inSize[0]):
        for j in xrange(inSize[1]):
            pixel = inPix[i,j]
            phi = i * 2 * pi / inSize[0]
            theta = j * pi / inSize[1]
            res = projection(theta,phi)
            (x,y) = cubeToImg(res,edge)
            #if i % 100 == 0 and j % 100 == 0:
            #    print i,j,phi,theta,res,x,y
            if x >= outSize[0]:
                #print "x out of range ",x,res
                x=outSize[0]-1
            if y >= outSize[1]:
                #print "y out of range ",y,res
                y=outSize[1]-1
            outPix[x,y] = pixel

imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convert(imgIn,imgOut)
imgOut.show()

projection 函数采用 thetaphi 值并返回立方体中每个方向上从 -1 到 1 的坐标。 cubeToImg 获取 (x,y,z) 坐标并将它们转换为输出图像坐标。

上述算法似乎使用image of buckingham palace 获得了正确的几何图形。我们得到:

这似乎使铺路中的大部分线条都正确。

我们得到了一些图像伪像。这是由于没有像素的一对一映射。我们需要做的是使用逆变换。我们不是循环遍历源中的每个像素并找到目标中的对应像素,而是循环遍历目标图像并找到最接近的对应源像素。

import sys
from PIL import Image
from math import pi,sin,cos,tan,atan2,hypot,floor
from numpy import clip

# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# face is face number
# edge is edge length
def outImgToXYZ(i,j,face,edge):
    a = 2.0*float(i)/edge
    b = 2.0*float(j)/edge
    if face==0: # back
        (x,y,z) = (-1.0, 1.0-a, 3.0 - b)
    elif face==1: # left
        (x,y,z) = (a-3.0, -1.0, 3.0 - b)
    elif face==2: # front
        (x,y,z) = (1.0, a - 5.0, 3.0 - b)
    elif face==3: # right
        (x,y,z) = (7.0-a, 1.0, 3.0 - b)
    elif face==4: # top
        (x,y,z) = (b-1.0, a -5.0, 1.0)
    elif face==5: # bottom
        (x,y,z) = (5.0-b, a-5.0, -1.0)
    return (x,y,z)

# convert using an inverse transformation
def convertBack(imgIn,imgOut):
    inSize = imgIn.size
    outSize = imgOut.size
    inPix = imgIn.load()
    outPix = imgOut.load()
    edge = inSize[0]/4   # the length of each edge in pixels
    for i in xrange(outSize[0]):
        face = int(i/edge) # 0 - back, 1 - left 2 - front, 3 - right
        if face==2:
            rng = xrange(0,edge*3)
        else:
            rng = xrange(edge,edge*2)

        for j in rng:
            if j<edge:
                face2 = 4 # top
            elif j>=2*edge:
                face2 = 5 # bottom
            else:
                face2 = face

            (x,y,z) = outImgToXYZ(i,j,face2,edge)
            theta = atan2(y,x) # range -pi to pi
            r = hypot(x,y)
            phi = atan2(z,r) # range -pi/2 to pi/2
            # source img coords
            uf = ( 2.0*edge*(theta + pi)/pi )
            vf = ( 2.0*edge * (pi/2 - phi)/pi)
            # Use bilinear interpolation between the four surrounding pixels
            ui = floor(uf)  # coord of pixel to bottom left
            vi = floor(vf)
            u2 = ui+1       # coords of pixel to top right
            v2 = vi+1
            mu = uf-ui      # fraction of way across pixel
            nu = vf-vi
            # Pixel values of four corners
            A = inPix[ui % inSize[0],clip(vi,0,inSize[1]-1)]
            B = inPix[u2 % inSize[0],clip(vi,0,inSize[1]-1)]
            C = inPix[ui % inSize[0],clip(v2,0,inSize[1]-1)]
            D = inPix[u2 % inSize[0],clip(v2,0,inSize[1]-1)]
            # interpolate
            (r,g,b) = (
              A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
              A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
              A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )

            outPix[i,j] = (int(round(r)),int(round(g)),int(round(b)))

imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convertBack(imgIn,imgOut)
imgOut.save(sys.argv[1].split('.')[0]+"Out2.png")
imgOut.show()

这样的结果是:

如果有人想反过来,请参阅this JS Fiddle page。

【讨论】:

是的,切片并不是真正的问题。困难的部分是投影的歪斜和翘曲,因此图像适合立方体贴图。 你应该把这个打包扔到github上。 我用纯 python 替换了 numpy.clip 并获得了更好的(130 秒到 50 秒)性能(numpy.clip 用于数字列表)例如:A = inPix[int(ui % inSize[ 0]),sorted([0, vi, inSize[1]-1])[1]] 在做一个项目时,我将这个算法实现为一个独立的 C++ 应用程序 => github.com/denivip/panorama 嘿,伙计们,我用很多 numpy 技巧重写了这个,并使用 opencv 进行像素插值(不是真的需要..),它在 8000x4000 图像上从 244 秒变为 7 秒。使用此方法,您可以生成一次映射,并在多个文件上快速使用它(使用 cv2 重映射)pastebin.com/Eeki92Zv【参考方案2】:

鉴于公认的优秀答案,我想添加我的相应 C++ 实现,基于 OpenCV。

对于那些不熟悉 OpenCV 的人,将Mat 视为图像。我们首先构建两个从 equirectangular 图像重新映射到我们相应的立方体贴图面的贴图。然后,我们使用 OpenCV 进行繁重的工作(即使用插值重新映射)。

如果不考虑可读性,代码可以更紧凑。

// Define our six cube faces.
// 0 - 3 are side faces, clockwise order
// 4 and 5 are top and bottom, respectively
float faceTransform[6][2] =

    0, 0,
    M_PI / 2, 0,
    M_PI, 0,
    -M_PI / 2, 0,
    0, -M_PI / 2,
    0, M_PI / 2
;

// Map a part of the equirectangular panorama (in) to a cube face
// (face). The ID of the face is given by faceId. The desired
// width and height are given by width and height.
inline void createCubeMapFace(const Mat &in, Mat &face,
        int faceId = 0, const int width = -1,
        const int height = -1) 

    float inWidth = in.cols;
    float inHeight = in.rows;

    // Allocate map
    Mat mapx(height, width, CV_32F);
    Mat mapy(height, width, CV_32F);

    // Calculate adjacent (ak) and opposite (an) of the
    // triangle that is spanned from the sphere center
    //to our cube face.
    const float an = sin(M_PI / 4);
    const float ak = cos(M_PI / 4);

    const float ftu = faceTransform[faceId][0];
    const float ftv = faceTransform[faceId][1];

    // For each point in the target image,
    // calculate the corresponding source coordinates.
    for(int y = 0; y < height; y++) 
        for(int x = 0; x < width; x++) 

            // Map face pixel coordinates to [-1, 1] on plane
            float nx = (float)y / (float)height - 0.5f;
            float ny = (float)x / (float)width - 0.5f;

            nx *= 2;
            ny *= 2;

            // Map [-1, 1] plane coords to [-an, an]
            // thats the coordinates in respect to a unit sphere
            // that contains our box.
            nx *= an;
            ny *= an;

            float u, v;

            // Project from plane to sphere surface.
            if(ftv == 0) 
                // Center faces
                u = atan2(nx, ak);
                v = atan2(ny * cos(u), ak);
                u += ftu;
             else if(ftv > 0) 
                // Bottom face
                float d = sqrt(nx * nx + ny * ny);
                v = M_PI / 2 - atan2(d, ak);
                u = atan2(ny, nx);
             else 
                // Top face
                float d = sqrt(nx * nx + ny * ny);
                v = -M_PI / 2 + atan2(d, ak);
                u = atan2(-ny, nx);
            

            // Map from angular coordinates to [-1, 1], respectively.
            u = u / (M_PI);
            v = v / (M_PI / 2);

            // Warp around, if our coordinates are out of bounds.
            while (v < -1) 
                v += 2;
                u += 1;
            
            while (v > 1) 
                v -= 2;
                u += 1;
            

            while(u < -1) 
                u += 2;
            
            while(u > 1) 
                u -= 2;
            

            // Map from [-1, 1] to in texture space
            u = u / 2.0f + 0.5f;
            v = v / 2.0f + 0.5f;

            u = u * (inWidth - 1);
            v = v * (inHeight - 1);

            // Save the result for this pixel in map
            mapx.at<float>(x, y) = u;
            mapy.at<float>(x, y) = v;
        
    

    // Recreate output image if it has wrong size or type.
    if(face.cols != width || face.rows != height ||
        face.type() != in.type()) 
        face = Mat(width, height, in.type());
    

    // Do actual resampling using OpenCV's remap
    remap(in, face, mapx, mapy,
         CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));

给定以下输入:

生成以下面孔:

图片由Optonaut提供。

【讨论】:

嗨!您的程序是正确的,但在 OpenCV 中,.at(x,y) 中的 x 和 y 是相反的,不是吗?应该是mapx.at&lt;float&gt;(y, x) = u; mapy.at&lt;float&gt;(y, x) = v; 如果 width = height 没问题,但可能会导致错误...但是如果您将 x 替换为 y,您的图像会旋转 :) 仍然可以正常工作,谢谢! 这是一个圆柱形 -> 立方体贴图,而不是 equirectangular.. 这就是为什么立方体贴图中的顶部和底部极点有黑色圆圈.. 因为圆柱形投影没有顶部和底部数据 @DavidJeske 它绝对是等角矩形。输入全景图(来自我的收藏)只是缺少顶部和底部。如果您使用“完整”全景图运行代码,则不会有任何漏洞。【参考方案3】:

更新 2:看起来有人拥有already built a far superior web application 而不是我自己的。他们的转换在客户端运行,因此无需担心任何上传和下载。

我想如果你因为某种原因讨厌 JavaScript,或者正试图在你的手机上这样做,那么我下面的 Web 应用程序是可以的。

更新:我已经发布了a simple web application,您可以在其中上传全景照片并让它以 ZIP 文件的形式返回六张 skybox 图像。

来源是对以下内容的清理重新实现,是available on GitHub。

该应用程序目前在单个免费层 Heroku dyno 上运行,但请不要尝试将其用作 API。如果您想要自动化,请自行部署; 单击 Deploy to Heroku 可用

原始:这是Salix Alba's absolutely fantastic answer 的(天真)修改版本,一次转换一张脸,输出六张不同的图像并保留原始图像的文件类型。

除了大多数用例可能需要六张单独的图像之外,一次转换一张人脸的主要优点是它可以减少处理大量图像的内存占用。

#!/usr/bin/env python
import sys
from PIL import Image
from math import pi, sin, cos, tan, atan2, hypot, floor
from numpy import clip

# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# faceIdx is face number
# faceSize is edge length
def outImgToXYZ(i, j, faceIdx, faceSize):
    a = 2.0 * float(i) / faceSize
    b = 2.0 * float(j) / faceSize

    if faceIdx == 0: # back
        (x,y,z) = (-1.0, 1.0 - a, 1.0 - b)
    elif faceIdx == 1: # left
        (x,y,z) = (a - 1.0, -1.0, 1.0 - b)
    elif faceIdx == 2: # front
        (x,y,z) = (1.0, a - 1.0, 1.0 - b)
    elif faceIdx == 3: # right
        (x,y,z) = (1.0 - a, 1.0, 1.0 - b)
    elif faceIdx == 4: # top
        (x,y,z) = (b - 1.0, a - 1.0, 1.0)
    elif faceIdx == 5: # bottom
        (x,y,z) = (1.0 - b, a - 1.0, -1.0)

    return (x, y, z)

# convert using an inverse transformation
def convertFace(imgIn, imgOut, faceIdx):
    inSize = imgIn.size
    outSize = imgOut.size
    inPix = imgIn.load()
    outPix = imgOut.load()
    faceSize = outSize[0]

    for xOut in xrange(faceSize):
        for yOut in xrange(faceSize):
            (x,y,z) = outImgToXYZ(xOut, yOut, faceIdx, faceSize)
            theta = atan2(y,x) # range -pi to pi
            r = hypot(x,y)
            phi = atan2(z,r) # range -pi/2 to pi/2

            # source img coords
            uf = 0.5 * inSize[0] * (theta + pi) / pi
            vf = 0.5 * inSize[0] * (pi/2 - phi) / pi

            # Use bilinear interpolation between the four surrounding pixels
            ui = floor(uf)  # coord of pixel to bottom left
            vi = floor(vf)
            u2 = ui+1       # coords of pixel to top right
            v2 = vi+1
            mu = uf-ui      # fraction of way across pixel
            nu = vf-vi

            # Pixel values of four corners
            A = inPix[ui % inSize[0], clip(vi, 0, inSize[1]-1)]
            B = inPix[u2 % inSize[0], clip(vi, 0, inSize[1]-1)]
            C = inPix[ui % inSize[0], clip(v2, 0, inSize[1]-1)]
            D = inPix[u2 % inSize[0], clip(v2, 0, inSize[1]-1)]

            # interpolate
            (r,g,b) = (
              A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
              A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
              A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )

            outPix[xOut, yOut] = (int(round(r)), int(round(g)), int(round(b)))

imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
faceSize = inSize[0] / 4
components = sys.argv[1].rsplit('.', 2)

FACE_NAMES = 
  0: 'back',
  1: 'left',
  2: 'front',
  3: 'right',
  4: 'top',
  5: 'bottom'


for face in xrange(6):
  imgOut = Image.new("RGB", (faceSize, faceSize), "black")
  convertFace(imgIn, imgOut, face)
  imgOut.save(components[0] + "_" + FACE_NAMES[face] + "." + components[1])

【讨论】:

【参考方案4】:

我编写了一个脚本来将生成的立方体贴图切割成单独的文件(posx.png、negx.png、posy.png、negy.png、posz.png 和 negz.png)。它还将这 6 个文件打包成一个 .zip 文件。

来源在这里:https://github.com/dankex/compv/blob/master/3d-graphics/skybox/cubemap-cut.py

你可以修改数组来设置图片文件:

name_map = [ \
 ["", "", "posy", ""],
 ["negz", "negx", "posz", "posx"],
 ["", "", "negy", ""]]

转换后的文件是:

【讨论】:

【参考方案5】:

首先:除非您真的必须自己转换图像(即,由于某些特定的软件要求),否则不要

原因是,虽然等角投影和三次投影之间有一个非常简单的映射,区域之间的映射并不简单:当你在你的特定点之间建立对应关系时目标图像和具有基本计算的源中的一个点,只要通过四舍五入将两个点都转换为像素你正在做一个不考虑的非常原始近似像素的大小,画质肯定低。

第二:即使您需要在运行时进行转换,您确定需要进行转换吗?除非有一些非常严格的性能问题,如果你只需要一个天空盒,创建一个非常大的球体,在上面缝合 equirectangular 纹理,然后就可以了。 Three.js 已经提供了球体,据我所知 ;-)

第三:NASA 提供了一种在所有可以想象的投影之间进行转换的工具(我刚刚发现并对其进行了测试,并且效果很好)。你可以在这里找到它:

G.Projector — Global Map Projector

而且我认为这些人知道他们在做什么是合理的 ;-)

事实证明,“家伙”在某种程度上知道他们在做什么:生成的立方体贴图有一个可怕的边框,这使得转换变得不那么容易......

我找到了将 equirectangular 转换为立方体贴图的权威工具,它被称为 erect2cubic

这是一个小实用程序,可以生成一个脚本,以这种方式提供给hugin:

erect2cubic --erect=input.png --ptofile=cube.pto
nona -o cube_prefix cube.pto

(从Vinay's Hacks页面中提取的信息)

它将生成所有六个立方体贴图面。我将它用于我的项目,它就像一个魅力

这种方法的唯一缺点是脚本 erect2cubit 不在标准的 Ubuntu 发行版中(这是我正在使用的),我不得不求助于 a blog describing how to install and use erect2cubic 了解如何安装它。

完全值得!

【讨论】:

我简直不敢相信直立立方和诺娜有多糟糕。如果他们无法读取任何源文件,两者都将失败而不会出错。 erect2cubic 不可能在 Windows 上运行,即使在 Linux 上也需要教程才能让它运行。 nona 不会在没有任何表单、消息或错误的情况下执行任何操作,因为源文件中有一个额外的通道。【参考方案6】:

cmft Studio 支持conversion/filtering 的各种HDR/LDR 投影到cubemaps

https://github.com/dariomanesku/cmftStudio

【讨论】:

【参考方案7】:

这是 Benjamin Dobell's code 的 JavaScript 版本。 convertFace 需要传递两个 ìmageData 对象和一个人脸 ID (0-6)。

提供的代码可以安全地在 web worker 中使用,因为它没有依赖关系。

// convert using an inverse transformation
function convertFace(imgIn, imgOut, faceIdx) 
    var inPix = shimImgData(imgIn),
                outPix = shimImgData(imgOut),
                faceSize = imgOut.width,
                pi = Math.PI,
                pi_2 = pi/2;

    for(var xOut=0; xOut<faceSize; xOut++) 
            for(var yOut=0; yOut<faceSize; yOut++) 

            var xyz = outImgToXYZ(xOut, yOut, faceIdx, faceSize);
            var theta = Math.atan2(xyz.y, xyz.x); // range -pi to pi
            var r = Math.hypot(xyz.x, xyz.y);
            var phi = Math.atan2(xyz.z, r); // range -pi/2 to pi/2

            // source image coordinates
            var uf = 0.5 * imgIn.width * (theta + pi) / pi;
            var vf = 0.5 * imgIn.width * (pi_2 - phi) / pi;

            // Use bilinear interpolation between the four surrounding pixels
            var ui = Math.floor(uf);  // coordinate of pixel to bottom left
            var vi = Math.floor(vf);
            var u2 = ui + 1;       // coordinates of pixel to top right
            var v2 = vi + 1;
            var mu = uf - ui;      // fraction of way across pixel
            var nu = vf - vi;

            // Pixel values of four corners
            var A = inPix.getPx(ui % imgIn.width, clip(vi, 0, imgIn.height-1));
            var B = inPix.getPx(u2 % imgIn.width, clip(vi, 0, imgIn.height-1));
            var C = inPix.getPx(ui % imgIn.width, clip(v2, 0, imgIn.height-1));
            var D = inPix.getPx(u2 % imgIn.width, clip(v2, 0, imgIn.height-1));

            // interpolate
            var rgb = 
              r:A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu + D[0]*mu*nu,
              g:A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu + D[1]*mu*nu,
              b:A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu + D[2]*mu*nu
            ;

            rgb.r = Math.round(rgb.r);
            rgb.g = Math.round(rgb.g);
            rgb.b = Math.round(rgb.b);

            outPix.setPx(xOut, yOut, rgb);

         // for(var yOut=0; yOut<faceSize; yOut++) ...

      // for(var xOut=0;xOut<faceSize;xOut++) ...

 // function convertFace(imgIn, imgOut, faceIdx) ...

// get x, y, z coordinates from out image pixels coordinates
// i,j are pixel coordinates
// faceIdx is face number
// faceSize is edge length
function outImgToXYZ(i, j, faceIdx, faceSize) 
    var a = 2 * i / faceSize,
            b = 2 * j / faceSize;

    switch(faceIdx) 
        case 0: // back
        return(x:-1, y:1-a, z:1-b);
    case 1: // left
        return(x:a-1, y:-1, z:1-b);
    case 2: // front
        return(x: 1, y:a-1, z:1-b);
    case 3: // right
        return(x:1-a, y:1, z:1-b);
    case 4: // top
        return(x:b-1, y:a-1, z:1);
    case 5: // bottom
        return(x:1-b, y:a-1, z:-1);
    
 // function outImgToXYZ(i, j, faceIdx, faceSize) ...

function clip(val, min, max) 
    return(val<min ? min : (val>max ? max : val));


function shimImgData(imgData) 
    var w = imgData.width*4,
            d = imgData.data;

    return(
        getPx:function(x, y) 
            x = x*4 + y*w;
            return([d[x], d[x+1], d[x+2]]);
        ,
        setPx:function(x, y, rgb) 
            x = x*4 + y*w;
            d[x] = rgb.r;
            d[x+1] = rgb.g;
            d[x+2] = rgb.b;
            d[x+3] = 255; // alpha
        
    );
 // function shimImgData(imgData) ...

【讨论】:

【参考方案8】:

我使用OpenGL 为这个问题创建了一个解决方案,并围绕它制作了一个命令行工具。它适用于图像和视频,是我发现的最快的工具。

Convert360 - GitHub 上的项目。

OpenGL Shader - 用于重新投影的片段着色器。

用法很简单:

pip install convert360
convert360 -i ~/Pictures/Barcelona/sagrada-familia.jpg -o example.png -s 300 300

要得到这样的东西:

【讨论】:

【参考方案9】:

环境贴图有多种表示形式。这是一个很好的概述。

Overview - Panoramic Images

如果您使用 Photosphere(或任何与此相关的全景应用程序),您很可能已经拥有水平的 latitude / longitude 表示。 然后,您可以简单地绘制一个带纹理的 three.js SphereGeometry。这是关于如何渲染地球的教程。

Tutorial - How to Make the Earth in WebGL?

祝你好运:)。

【讨论】:

【参考方案10】:

一个非常简单的 C++ 应用程序,它根据Salix Alba 的回答将 equirectangular 全景图转换为立方体贴图:

Photo Panorama Converter

【讨论】:

一张人脸图像存在问题。请在 git github.com/denivip/panorama/issues/6 中检查该问题 @gadlol 是的,看到了通知。感谢您的关注!我会在圣诞节后看看:-)【参考方案11】:

也许我在这里遗漏了一些东西。但似乎大多数(如果不是全部)呈现的转换代码可能有些不正确。他们拍摄球形全景图(等矩形——水平 360 度和垂直 180 度),并且似乎使用笛卡尔 圆柱变换转换为立方体面。他们不应该使用笛卡尔 球面变换吗?

Spherical Coordinates

我想只要他们反转计算以从立方体面到全景图,那么它应该可以解决。但是在使用球面变换时,立方体面的图像可能会略有不同。

如果我从这个等距矩形(球形全景)开始:

然后,如果我使用圆柱变换(此时我不能 100% 确定它是否正确),我会得到以下结果:

但是如果我使用球面变换,我会得到这样的结果:

它们不一样。但我的球面变换结果似乎与the result of Danke Xie 匹配,但他的链接没有显示他正在使用的那种变换,尽我所能阅读。

那么我是否误解了该主题的许多贡献者使用的代码?

【讨论】:

我有同样的问题,想知道它是否是源图像。但是,那些有问题的图像确实会被 sphere2cube 正确处理。你在这方面有什么进一步的进展吗? @ Longmang。不,我有我的脚本可以进行球形变换,它似乎可以工作。我仍然不明白那些声称使用圆柱形并将其称为球形的参考文献。 你愿意让我得到你的剧本吗?我对 OpenCV 等的了解很薄弱,所以我处于我能力的边缘 我的脚本是一个运行 ImageMagick 的 bash Unix shell 脚本,而不是 OpenCV。它可用于非商业用途,无需在fmwconcepts.com/imagemagick获得许可【参考方案12】:

kubi 可以从等角矩形图像转换为立方体面。我把它写得又快又灵活。它提供了选择输出布局的选项(默认为六个单独的图像)并决定重采样方法。

【讨论】:

我建议重命名它并给它一个合适的名字,比如Kubi

以上是关于将 2:1 equirectangular 全景图转换为立方体贴图的主要内容,如果未能解决你的问题,请参考以下文章

全景图转小行星视角投影原理详解

统一录制 360° 视频不是 equirectangular

3D全景效果图如何制作?

怎么制作vr全景图

Fisheye-Equirectangular 转换

max如何渲染720度全景图