Convert 2:1 equirectangular panorama to cube map

前端 未结 11 1148
情深已故
情深已故 2020-11-30 17:30

I\'m currently working on a simple 3D panorama viewer for a website. For mobile performance reasons I\'m using the three.js CSS3 renderer. This requires a cube

相关标签:
11条回答
  • 2020-11-30 17:49

    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

    0 讨论(0)
  • 2020-11-30 17:54

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

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

            // 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) {...}
    
    0 讨论(0)
  • 2020-11-30 17:56

    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.

    Convert360 - Project on GitHub.

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

    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:

    0 讨论(0)
  • 2020-11-30 17:58

    cmft Studio supports conversion/filtering of various HDR/LDR projections to cubemaps.

    https://github.com/dariomanesku/cmftStudio

    0 讨论(0)
  • 2020-11-30 17:59

    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

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

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

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

    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?

    0 讨论(0)
  • 2020-11-30 18:04

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

    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.

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

    Source is a cleaned up reimplementation of what's below, and is available on 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.

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

    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])
    
    0 讨论(0)
提交回复
热议问题