Converting 360 degree view to equirectangular in node js?

雨燕双飞 提交于 2019-12-04 06:07:36

You are asking how to convert a 360deg fish-eye projection to an equirectangular projection.

In order to do this, for every pixel on the fish-eye image you need to know where to place in onto the output image.

Your input image is 1920x1080, let us assume you want to output it to an equirectangular projection of the same size.

The input circle mapping is defined as:

cx = 960; // center of circle on X-axis
cy = 540; // center of circle on Y-axis
radius = 540; // radius of circle

If you have a pixel at (x,y) in the input image, then we can calculate the spherical coordinates using:

dx = (x - cx) * 1.0 / radius;
dy = (y - cy) * 1.0 / radius;
theta_deg = atan2(dy, dx) / MATH_PI * 180;
phi_deg = acos(sqrt(dx*dx + dy*dy)) / MATH_PI * 180;
outputx = (theta_deg + 180) / 360.0 * outputwidth_px;
outputy = (phi_deg + 90) / 180.0 * outputheight_px;

So there we translated (x,y) from the fish-eye image to the (outputx,outputy) in the equirectangular image. In order to not leave the implementation as the dreaded "exercise to the reader", here is some sample Javascript-code using the Jimp-library as used by the OP:

var jimp = require('jimp');
var inputfile = 'input.png';
jimp.read(inputfile, function(err, inputimage)
{
    var cx = 960;
    var cy = 540;
    var radius = 540;
    var inputwidth = 1920;
    var inputheight = 1080;
    var outputwidth = 1920;
    var outputheight = 1080;
    new jimp(outputwidth, outputheight, 0x000000ff, function(err, outputimage)
    {
        for(var y=0;y<inputheight;++y)
        {
            for(var x=0;x<inputwidth;++x)
            {
                var color = inputimage.getPixelColor(x, y);
                var dx = (x - cx) * 1.0 / radius;
                var dy = (y - cy) * 1.0 / radius;
                var theta_deg = Math.atan2(dy, dx) / Math.PI * 180;
                var phi_deg = Math.acos(Math.sqrt(dx*dx + dy*dy)) / Math.PI * 180;
                var outputx = Math.round((theta_deg + 180) / 360.0 * outputwidth);
                var outputy = Math.round((phi_deg + 90) / 180.0 * outputheight);
                outputimage.setPixelColor(color, outputx, outputy);
            }
        }
        outputimage.write('output.png');
    });
});

Note that you will still need to do blending of the pixel with neighbouring pixels (for the same reason as when you're resizing the image).

Additionally, in your case, you only have half of the sphere (you can't see the sun in the sky). So you would need to use var outputy = Math.round(phi_deg / 90.0 * outputheight). In order to keep the right aspect ratio, you might want to change the height to 540.

Also note that the given implementation may not be efficient at all, it's better to use the buffer directly.

Anyway, without blending I came up with the result as demonstrated here:


So in order to do blending, you could use the simplest method which is the nearest neighbour approach. In that case, you should invert the formulas in the above example. Instead of moving the pixels from the input image to the right place in the output image, you can go through every pixel in the output image and ask which input pixel we can use for that. This will avoid the black pixels, but may still show artifacts:

var jimp = require('jimp');
var inputfile = 'input.png';
jimp.read(inputfile, function(err, inputimage)
{
    var cx = 960;
    var cy = 540;
    var radius = 540;
    var inputwidth = 1920;
    var inputheight = 1080;
    var outputwidth = 1920;
    var outputheight = 1080/2;
    var blendmap = {};
    new jimp(outputwidth, outputheight, 0x000000ff, function(err, outputimage)
    {
        for(var y=0;y<outputheight;++y)
        {
            for(var x=0;x<outputwidth;++x)
            {
                var theta_deg = 360 - x * 360.0 / outputwidth - 180;
                var phi_deg = 90 - y * 90.0 / outputheight;
                var r = Math.sin(phi_deg * Math.PI / 180)
                var dx = Math.cos(theta_deg * Math.PI / 180) * r;
                var dy = Math.sin(theta_deg * Math.PI / 180) * r;
                var inputx = Math.round(dx * radius + cx);
                var inputy = Math.round(dy * radius + cy);
                outputimage.setPixelColor(inputimage.getPixelColor(inputx, inputy), x, y);
            }
        }
        outputimage.write('output.png');
    });
});

For reference, in order to convert between Cartesian and Spherical coordinate systems. These are the formulas (taken from here). Note that the z is in your case just 1, a so-called "unit" sphere, so you can just leave it out of the equations. You should also understand that since the camera is actually taking a picture in three dimensions, you also need formulas to work in three dimensions.

Here is the generated output image:

Since I don't see your original input image in your question anymore, in order for anyone to test the code from this answer, you can use the following image:

Run the code with:

mkdir /tmp/test
cd /tmp/test
npm install --permanent jimp
cat <<EOF >/tmp/test/main.js
... paste the javascript code from above ...
EOF
curl https://i.stack.imgur.com/0zWt6.png > input.png
node main.js

Note: In order to further improve the blending, you should remove the Math.round. So for instance, if you need to grab a pixel at x is 0.75, and the pixel on the left at x = 0 is white, and the pixel on the right at x = 1 is black. Then you want to mix both colors into a dark grey color (using ratio 0.75). You would have to do this for both dimensions simultaneously, if you want a nice result. But this should really be in a new question imho.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!