Javascript 将 2:1 等距柱状图全景图转换为立方体贴图

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29678510/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 03:47:00  来源:igfitidea点击:

Convert 2:1 equirectangular panorama to cube map

javascriptmaththree.js

提问by oelna

I'm currently working on a simple 3D panorama viewer for a website. For mobile performance reasons I'm using the three.jsCSS3 renderer. This requires a cube map, split up into 6 single images.

我目前正在为一个网站开发一个简单的 3D 全景查看器。出于移动性能的原因,我使用的是three.jsCSS3 渲染器。这需要一个立方体贴图,分成 6 个单张图像。

I'm recording the images on the iPhone with the Google Photosphere app, or similar apps that create 2:1 equirectangular panoramas. I then resize and convert these to a cubemap with this website: http://gonchar.me/panorama/(Flash)

我正在使用 Google Photosphere 应用程序或创建 2:1 等距柱状图全景图的类似应用程序在 iPhone 上录制图像。然后我通过这个网站调整大小并将它们转换为立方体贴图:http: //gonchar.me/panorama/(Flash)

Preferrably, I'd like to do the conversion myself, either on the fly in three.js, if that's possible, or in Photoshop. I found Andrew Hazelden's Photoshop actions, and they seem kind of close, but no direct conversion is available. Is there a mathematical way to convert these, or some sort of script that does it? I'd like to avoid going through a 3D app like Blender, if possible.

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

Maybe this is a long shot, but I thought I'd ask. I have okay experience with javascript, but I'm pretty new to three.js. I'm also hesitant to rely on the WebGL functionality, since it seems either slow or buggy on mobile devices. Support is also still spotty.

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

回答by Salix alba

If you want to do it server side there are many options. http://www.imagemagick.org/has a bunch of command line tools which could slice your image into pieces. You could put the command to do this into a script and just run that each time you have a new image.

如果你想做服务器端,有很多选择。http://www.imagemagick.org/有一堆命令行工具可以将您的图像切成碎片。您可以将执行此操作的命令放入脚本中,并在每次获得新图像时运行该脚本。

Its hard to tell quite what algorithm is used in the program. We can try and reverse engineer quite what is happening by feeding a square grid into the program. I've used a grid from wikipedia

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

64 by 64 grid

64 x 64 网格

Which gives projected gridThis gives us a clue as to how the box is constructed.

这给投影网格了我们一个关于盒子是如何构造的线索。

Imaging sphere with lines of latitude and longitude one it, and a cube surrounding it. Now project from the point at center of the sphere produces a distorted grid on the cube.

带有纬度和经度线的成像球体和一个立方体围绕它。现在从球体中心的点投影会在立方体上产生扭曲的网格。

Mathematically take polar coordinates r, θ, ?, for the sphere r=1, 0 < θ < π, -π/4 < ? < 7π/4

数学上取极坐标r, θ, ?, 对于球体r=1, 0 < θ < π, -π/4 < ? < 7π/4

  • x= r sin θ cos ?
  • y= r sin θ sin ?
  • z= r cos θ
  • x= r sin θ cos ?
  • y= r sin θ sin ?
  • z= r cos θ

centrally project these to the cube. First we divide into four regions by the latitude -π/4 < ? < π/4, π/4 < ? < 3π/4, 3π/4 < ? < 5π/4, 5π/4 < ? < 7π/4. These will either project to one of the four sides the top or the bottom.

将这些集中投影到立方体。首先我们将纬度划分为四个区域 -π/4 < ? < π/4, π/4 < ? < 3π/4, 3π/4 < ? < 5π/4, 5π/4 < ? < 7π/4。这些将投射到顶部或底部的四个侧面之一。

Assume we are in the first side -π/4 < ? < π/4. The central projection of (sin θ cos ?, sin θ sin ?, cos θ) will be (a sin θ cos ?, a sin θ sin ?, a cos θ) which hits the x=1 plane when

假设我们在第一边 -π/4 < ? < π/4。(sin θ cos ?, sin θ sin ?, cos θ) 的中心投影将是 (a sin θ cos ?, a sin θ sin ?, a cos θ) 当

  • a sin θ cos ? = 1
  • a sin θ cos ? = 1

so

所以

  • a = 1 / (sin θ cos ?)
  • a = 1 / (sin θ cos ?)

and the projected point is

并且投影点是

  • (1, tan ?, cot θ / cos ?)
  • (1, tan ?, cot θ / cos ?)

If | cot θ / cos ? | < 1 this will be on the front face. Otherwise, it will be projected on the top or bottom and you will need a different projection for that. A better test for the top uses the fact that the minimum value of cos ? will be cos π/4 = 1/√2, so the projected point is always on the top if cot θ / (1/√2) > 1 or tan θ < 1/√2. This works out as θ < 35o or 0.615 radians.

如果 | cot θ / cos ? | < 1 这将在正面。否则,它将被投影在顶部或底部,您将需要不同的投影。对顶部的更好测试使用 cos 的最小值这一事实?将是 cos π/4 = 1/√2,所以如果 cot θ / (1/√2) > 1 或 tan θ < 1/√2,投影点总是在顶部。计算结果为 θ < 35o 或 0.615 弧度。

Put this together in python

把它放在 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()

The projectionfunction takes the thetaand phivalues and returns coordinates in a cube from -1 to 1 in each direction. The cubeToImg takes the (x,y,z) coords and translates them to the output image coords.

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

The above algorithm seems to get the geometry right using an image of buckingham palacewe get cube map of buckingham palaceThis seems to get most of the lines in the paving right.

上面的算法似乎使用我们得到 的白金汉宫图像获得了正确的几何形状白金汉宫的立方体地图这似乎使铺路中的大部分线正确。

We are getting a few image artefacts. This is due to not having a 1 to 1 map of pixels. What we need to do is use a inverse transformation. Rather than loop through each pixel in the source and find the corresponding pixel in the target we loop through the target images and find the closest corresponding source pixel.

我们得到了一些图像人工制品。这是因为没有 1 比 1 的像素图。我们需要做的是使用逆变换。不是遍历源中的每个像素并找到目标中的相应像素,而是遍历目标图像并找到最接近的相应源像素。

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()

The results of this are Using the inverse transformation

这样做的结果是 使用逆变换

回答by Emiswelt

Given the excellent accepted answer, I wanted to add my corresponding c++ implementation, based on OpenCV.

鉴于优秀的公认答案,我想添加我相应的c++ implementation,基于OpenCV

For those not familiar with OpenCV, think of Matas an image. We first construct two maps that remap from the equirectangular image to our corresponding cubemap face. Then, we do the heavy lifting (i.e. remapping with interpolation) using OpenCV.

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

The code can be made more compact, if readability is not of concern.

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

// 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));
}

Given the following input:

给定以下输入:

enter image description here

在此处输入图片说明

The following faces are generated:

生成以下面:

enter image description here

在此处输入图片说明

Image courtesy of Optonaut.

图片由Optonaut 提供

回答by Danke Xie

I wrote a script to cut the generated cubemap into individual files (posx.png, negx.png, posy.png, negy.png, posz.png and negz.png). It will also pack the 6 files into a .zip file.

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

The source is here: https://github.com/dankex/compv/blob/master/3d-graphics/skybox/cubemap-cut.py

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

You can modify the array to set the image files:

您可以修改数组以设置图像文件:

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

The converted files are:

转换后的文件是:

enter image description hereenter image description hereenter image description hereenter image description hereenter image description hereenter image description here

在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明在此处输入图片说明

回答by Rick77

Found this question, and even though the answers are good, I think there is still some ground uncovered, so here are my two cents.

找到了这个问题,尽管答案很好,但我认为仍有一些不足之处,所以这是我的两分钱。

First: unless you really have to convert the images yourself (i.e. because of some specific software requirement), don't.

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

The reason is that, even though there is a very simple mapping between equirectangular projection and cubic projection, the mapping between the areas is not simple: when you establish a correspondence between a specific point of your destination image and a point in the source with an elementary computation, as soon as you convert both points to pixels by rounding you are doing a veryraw approximationthat doesn't consider the size of the pixels, and the quality of the image is bound to be low.

原因是,即使等距柱状投影和三次投影之间有一个非常简单的映射,区域之间的映射并不简单:当您在目标图像的特定点和源中的点之间建立对应关系时,基本计算,一旦您通过四舍五入将两个点都转换为像素,您就会进行非常原始的近似,它不考虑像素的大小,并且图像的质量肯定会很低。

Second: even if you need to do the conversion at runtime, are you sure that you need to do the conversion at all? Unless there is some very stringent performance problem, if you just need a skybox, create a very big sphere, stitch the equirectangular texure on it, and off you go. Three JS provides the sphere already, as far as I remember ;-)

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

Third: NASA provides a tool to convert between all conceivable projections (I just found out, tested it, and works like a charm). You can find it here:

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

G.Projector — Global Map Projector

G.Projector — 全球地图投影仪

and I find reasonable to think that the guys know what they are doing ;-)

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

Hope this helps

希望这可以帮助

UPDATE:it turns out that the "guys" know what they do up to some point: the generated cubemap has an hideous border which makes the conversion not that easy...

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

UPDATE 2:found the definitive tool for equirectangular to cubemap conversion, and it's called erect2cubic.

更新 2:找到了等距柱状图到立方体贴图转换的权威工具,它被称为erect2cubic.

It's a small utility that generates a script to be fed to hugin, in this way:

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

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

(information siphoned from Vinay's Hackspage)

(信息来自Vinay 的 Hacks页面)

and will generate all 6 cubemap faces. I'm using it for my project and it works like a charm!

并将生成所有 6 个立方体贴图面。我在我的项目中使用它,它就像一个魅力

The only downside of this approach is that the script erect2cubitit's not in the standard Ubuntu distribution (which is what I'm using) and I had to resort to the instructions at this link:

这种方法的唯一缺点是它的脚本erect2cubit不在标准的 Ubuntu 发行版中(这是我正在使用的),我不得不求助于此链接中的说明:

Blog describing how to install and use erect2cubic

描述如何安装和使用直立2立方体的博客

to find out how to install it.

以了解如何安装它。

Totally worth it!

完全值得的!

回答by Benjamin Dobell

UPDATE 2: It looks like someone else had alreadybuilt a far superior web appthan my own. Their conversion runs client side, so there's no uploads/downloads to worry about.

更新 2:看起来其他人已经构建了一个比我自己的网络应用程序优越得多的网络应用程序。他们的转换在客户端运行,因此无需担心上传/下载。

I suppose if you hate JS for some reason, or are trying to do this on your mobile, then my web app below is okay.

我想如果您出于某种原因讨厌 JS,或者正在尝试在您的手机上执行此操作,那么我下面的 Web 应用程序还可以。

UPDATE: I've published a simple web appwhere you can upload a panorama and have it return the 6 skybox images in a zip.

更新:我发布了一个简单的网络应用程序,您可以在其中上传全景图并让它以 zip 格式返回 6 个天空盒图像。

Source is a cleaned up reimplementation of what's below, and is available on Github.

Source 是对以下内容的清理重新实现,可在Github 上找到

The app is presently running on a single free-tier Heroku dyno, please don't attempt to use it as an API. If you want automation, make your own deployment; single click Deploy to Heroku available.

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

ORIGINAL: Here's a (naively) modified version of Salix Alba's absolutely fantastic answerthat converts one face at a time, spits out six different images and preserves the original image's file type.

原文:这是Salix Alba 绝对精彩的答案的(天真)修改版本,一次转换一张脸,吐出六个不同的图像并保留原始图像的文件类型。

Aside from the fact most use cases probably expect six separate images, the main advantage of converting one face at a time is that it makes working with huge images a lot less memory intensive.

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

#!/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])

回答by planetboy

cmftStudiosupports conversion/filteringof various HDR/LDRprojections to cubemaps.

cmft工作室支持conversion/filtering各种的HDR/LDR突起到cubemaps

https://github.com/dariomanesku/cmftStudio

https://github.com/dariomanesku/cmftStudio

回答by knee-cola

Here's a JavaScript version of Benjamn Dobell's code. The convertFaceneeds to be passed two ìmageDataobjects and a face ID (0-6).

这是 Benjamn Dobell 代码的 JavaScript 版本。的convertFace需要被传两个ìmageData对象和面ID(0-6)。

The provided code can safely be used in a web worker, since it has no dependencies.

提供的代码可以安全地用于 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 img coords
                    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);  // coord of pixel to bottom left
                    var vi = Math.floor(vf);
                    var u2 = ui+1;       // coords 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 coords from out image pixels coords
        // i,j are pixel coords
        // 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) {...}

回答by Mateus Zitelli

I created a solution for this problem using OpenGL and made a command line tool around it. It works both with images and videos, and it is the fastest tool that I found out there.

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

Convert360- Project on GitHub.

Convert360- GitHub 上的项目。

OpenGL Shader- The fragment shader used for the re-projection.

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

The usage is as simple as:

用法很简单:

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

To get something like this:

要得到这样的东西:

enter image description here

在此处输入图片说明

回答by fmw42

Perhaps I am missing something here. But it seems that most if not all the presented transformation code may be somewhat incorrect. They take a spherical panorama (equirectangular --- 360 deg horizontally and 180 deg vertically) and seem to convert to the cube faces using a cartesian <-> cylindrical transformation. Should they not be using a cartesian <-> spherical transformation. See http://mathworld.wolfram.com/SphericalCoordinates.html

也许我在这里遗漏了一些东西。但似乎大多数(如果不是全部)呈现的转换代码可能有些不正确。它们采用球形全景图(等距柱状图 --- 水平 360 度和垂直 180 度),似乎使用笛卡尔 <-> 圆柱变换转换为立方体面。他们是否应该不使用笛卡尔 <-> 球面变换。见http://mathworld.wolfram.com/SphericalCoordinates.html

I suppose that as long as they reverse the calculation to go from the cube faces to the panorama, then it should work out. But the images of the cube faces may be slightly different when using the spherical transformation.

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

If I start with this equirectangular (spherical panorama):

如果我从这个等距柱状图(球面全景图)开始:

enter image description here

在此处输入图片说明

Then if I use a cylindrical transformation (which I am not 100% sure is correct at this time), I get this result:

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

enter image description here

在此处输入图片说明

But if I use a spherical transformation, I get this result:

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

enter image description here

在此处输入图片说明

They are not the same. But my spherical transformation result seems to match the result of Danke Xie, but his link does not show the kind of transformation he is using, as best I can read it.

她们不一样。但是我的球面变换结果似乎与谢丹克的结果相匹配,但是他的链接没有显示他使用的变换类型,尽我所能阅读。

So am I misunderstanding the code being used by many of the contributors to this topic?

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

回答by Denis Bulichenko

A very simple C++ app which converts an equirectangular panorama to cube map based on the answer by Salix Alba=> https://github.com/denivip/panorama

一个非常简单的 C++ 应用程序,它根据Salix Alba的答案将等距柱状全景图转换为立方体贴图=> https://github.com/denivip/panorama