关于 javascript:在节点 js 中将 360 度视图转换为 equirectangular?

Converting 360 degree view to equirectangular in node js?

在过去的两天里,我一直在尝试将 360 度相机、单鱼眼图像转换为节点 js 中的 equirectangular 查看器。在 stackoverflow 中,同样的问题在伪代码中被询问和回答。我一直在尝试将伪代码转换为节点 js 并清除了一些错误。现在项目运行没有错误,但输出图像是空白的。

从那个伪,我不知道polar_w,polar_h和geo_w,geo_h,geo和极坐标值,所以,它给出了静态值来显示输出。这是我将伪代码转换为节点 js 的链接。
如何将球坐标转换为等角投影坐标?.

这是我尝试将球面图像转换为 equirectangular 查看器的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
 exports.sphereImage=(request, response)=>{


 var Jimp = require('jimp');


  // Photo resolution

var img_w_px = 1280;
var img_h_px = 720;

var polar_w = 1280;
var polar_h = 720;
var geo_w = 1280;
var geo_h = 720;

var img_h_deg = 70;
var img_w_deg = 30;

  // Camera field-of-view angles

var img_ha_deg = 70;
var img_va_deg = 40;


  // Camera rotation angles

 var  hcam_deg = 230;
 var  vcam_deg = 60;

  // Camera rotation angles in radians

 var hcam_rad = hcam_deg/180.0*Math.PI;
 var vcam_rad = vcam_rad/180.0*Math.PI;

 // Rotation around y-axis for vertical rotation of camera

  var rot_y = [

   [Math.cos(vcam_rad), 0, Math.sin(vcam_rad)],
   [0, 1, 0],
  [-Math.sin(vcam_rad), 0, Math.cos(vcam_rad)]

    ];

 // Rotation around z-axis for horizontal rotation of camera

  var rot_z = [

 [Math.cos(hcam_rad), -Math.sin(hcam_rad), 0],
 [Math.sin(hcam_rad), Math.cos(hcam_rad), 0],
 [0, 0, 1]

 ];


  Jimp.read('./public/images/4-18-2-42.jpg', (err, lenna) => {


    polar = new Jimp(img_w_px, img_h_px);
    geo = new Jimp(img_w_px, img_h_px);

  for(var i=0; i<img_h_px; ++i)
  {
  for(var j=0; j<img_w_px; ++j)
  {
    // var p = img.getPixelAt(i, j);

    var p = lenna.getPixelColor(i, j)
    // var p = getPixels(img, { x: i, y: j })

    // Calculate relative position to center in degrees
    var p_theta = (j - img_w_px / 2.0) / img_w_px * img_w_deg / 180.0 * Math.PI;
    var p_phi = -(i - img_h_px / 2.0) / img_h_px * img_h_deg / 180.0 *Math. PI;

    // Transform into cartesian coordinates
    var p_x = Math.cos(p_phi) * Math.cos(p_theta);
    var p_y = Math.cos(p_phi) * Math.sin(p_theta);
    var p_z = Math.sin(p_phi);
    var p0 = {p_x, p_y, p_z};

    // Apply rotation matrices (note, z-axis is the vertical one)
    // First vertically
    var p1 = rot_y[1][2][3] * p0;
    var p2 = rot_z[1][2][3] * p1;

    // Transform back into spherical coordinates
    var theta = Math.atan2(p2[1], p2[0]);
    var phi = Math.asin(p2[2]);

    // Retrieve longitude,latitude
    var longitude = theta / Math.PI * 180.0;
    var latitude = phi / Math.PI * 180.0;

    // Now we can use longitude,latitude coordinates in many different
    projections, such as:
    // Polar projection
    {
        var polar_x_px = (0.5*Math.PI + phi)*0.5 * Math.cos(theta)
   /Math.PI*180.0 * polar_w;
        var polar_y_px = (0.5*Math.PI + phi)*0.5 * Math.sin(theta)
    /Math.PI*180.0 * polar_h;
        polar.setPixelColor(p, polar_x_px, polar_y_px);
    }
    // Geographical (=equirectangular) projection
    {
        var geo_x_px = (longitude + 180) * geo_w;
        var geo_y_px = (latitude + 90) * geo_h;
        // geo.setPixel(geo_x_px, geo_y_px, p.getRGB());
        geo.setPixelColor(p, geo_x_px, geo_y_px);
    }
      // ...
 }
}

  geo.write('./public/images/4-18-2-42-00001.jpg');
 polar.write('./public/images/4-18-2-42-00002.jpg');



 });


}

并尝试了另一种方法,将图像分成四个部分来检测汽车。使用 image-slice 模块将图像切成四部分,并使用 jimp 模块进行读写。但不幸的是,没有正确检测到汽车。

这是我用于切片图像的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 exports.sliceImage=(request, response)=>{

var imageToSlices = require('image-to-slices');
var lineXArray = [540, 540];
var lineYArray = [960, 960];
var source = './public/images/4-18-2-42.jpg'; // width: 300, height: 300

imageToSlices(source, lineXArray, lineYArray, {
    saveToDir: './public/images/',
    clipperOptions: {
        canvas: require('canvas')
    }    
}, function() {
    console.log('the source image has been sliced into 9 sections!');
});


 }//sliceImage

为了从图像中检测汽车,我使用了 opencv4nodejs。未正确检测到汽车。这是我用于检测汽车的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
   function runDetectCarExample(img=null){
        if(img==null){

         img = cv.imread('./public/images/section-1.jpg');
    }else
    {
         img=cv.imread(img);
    }
        const minConfidence = 0.06;

        const predictions = classifyImg(img).filter(res => res.confidence > minConfidence && res.className=='car');

        const drawClassDetections = makeDrawClassDetections(predictions);

        const getRandomColor = () => new cv.Vec(Math.random() * 255, Math.random() * 255, 255);

        drawClassDetections(img, 'car', getRandomColor);
        cv.imwrite('./public/images/section-'+Math.random()+'.jpg', img);
        var name="distanceFromCamera";
        var focalLen= 1.6 ;//Focal length in mm
        var realObjHeight=254 ;//Real Height of Object in mm
        var cameraFrameHeight=960;//Height of Image in pxl
        var imgHeight=960;//Image Height in pxl
        var sensorHeight=10;//Sensor height in mm
        var R = 6378.1 //#Radius of the Earth
        var brng = 1.57 //#Bearing is 90 degrees converted to radians.
        var hc=(200/100);//Camera height in m
        predictions
            .forEach((data)=> {

                // imgHeight=img.rows;//Image Height in pxl
                // realObjHeight=data.rect.height;
                // data.rect[name]=((focalLen)*(realObjHeight)*
         (cameraFrameHeight))/((imgHeight)*(sensorHeight));

                var dc=(((data.rect.width * focalLen) / img.cols)*2.54)*100; // meters
                console.log(Math.floor(parseInt(data.rect.width)));
                // var dc=((Math.floor(parseInt(data.rect.width)* 0.264583) * focalLen) / img.cols); // mm


                var lat1=13.0002855;//13.000356;
                var lon1=80.2046441;//80.204632;
                // Gate 13.0002855,80.2046441
                // Brazil Polsec : -19.860566, -43.969436
                // var d=Math.sqrt((dc*dc)+(hc*hc));
                // d=(data.rect[name])/1000;
                data.rect[name]=d=dc/1000;
                lat1 =toRadians(lat1);
                lon1 = toRadians(lon1);
                brng =toRadians(90);
                // lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) +
                //      Math.cos(lat1)*Math.sin(d/R)*Math.cos(brng));

                // lon2 = lon1 +
             Math.atan2(Math.sin(brng)*Math.sin(d/R)*Math.cos(lat1),
                //              Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));


        var lat2 = Math.asin(Math.sin(lat1) * Math.cos(d/6371) +
                      Math.cos(lat1) * Math.sin(d/6371) * Math.cos(brng));

        var lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d/6371) * Math.cos(lat1),
                              Math.cos(d/6371) - Math.sin(lat1) * Math.sin(lat2));

                lat2 = toDegrees(lat2);
                lon2 = toDegrees(lon2);
                data.rect['latLong']=lat2+','+lon2;
                // console.log(brng);

            });




        response.send(predictions);
        cv.imshowWait('img', img);
    };

这里是需要转换成等距矩形的鱼眼图。

非常感谢任何帮助....


您在问如何将 360 度鱼眼投影转换为 equirectangular 投影。

为了做到这一点,对于鱼眼图像上的每个像素,您需要知道在输出图像上放置的位置。

您的输入图像是 1920x1080,让我们假设您要将其输出到相同大小的 equirectangular 投影。

输入圆映射定义为:

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

如果输入图像中有一个像素位于 (x,y) 处,那么我们可以使用以下方法计算球坐标:

1
2
3
4
5
6
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;

因此,我们将鱼眼图像中的 (x,y) 转换为 equirectangular 图像中的 (outputx,outputy)。为了不让实现成为可怕的"读者练习",这里是一些使用 OP 使用的 Jimp 库的示例 Javascript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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');
    });
});

请注意,您仍然需要将像素与相邻像素混合(与调整图像大小时的原因相同)。

此外,在您的情况下,您只有一半的球体(您看不到天空中的太阳)。所以你需要使用 var outputy = Math.round(phi_deg / 90.0 * outputheight)。为了保持正确的纵横比,您可能需要将高度更改为 540.

还要注意,给定的实现可能根本没有效率,最好直接使用缓冲区。

无论如何,在没有混合的情况下,我得出了如下所示的结果:
equirectangular

因此,为了进行混合,您可以使用最简单的方法,即最近邻方法。在这种情况下,您应该反转上面示例中的公式。无需将输入图像中的像素移动到输出图像中的正确位置,您可以遍历输出图像中的每个像素并询问我们可以使用哪个输入像素。这将避免黑色像素,但仍可能显示伪影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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');
    });
});

仅供参考,以便在笛卡尔坐标系和球坐标系之间进行转换。这些是公式(取自此处)。请注意,在您的情况下, z 只是 1,即所谓的"单位"球体,因此您可以将其排除在等式之外。您还应该了解,由于相机实际上是在三个维度上拍照,因此您还需要公式才能在三个维度上工作。

Spherical

这是生成的输出图像:

Output

1
2
3
4
5
6
7
8
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

注意:为了进一步改进混合,您应该删除 Math.round。因此,例如,如果您需要在 x 处抓取一个像素为 0.75,而 x = 0 处左侧的像素为白色,而 x = 1 处右侧的像素为黑色。然后你想将两种颜色混合成深灰色(使用比例 0.75)。如果您想要一个好的结果,您必须同时对两个维度执行此操作。但这真的应该是一个新问题恕我直言。