从街景上的像素获取标题和音高

时间:2014-02-05 10:15:49

标签: javascript math google-maps-api-3 geometry google-street-view

我认为这是这个问题的最佳位置。

我正在尝试获取嵌入式Google街景中任何点击点的标题和音高。

我知道并且可以得到的唯一信息是:

  • 视野(度)
  • 中心点的航向和俯仰(以度为单位)以及x和y像素位置
  • 鼠标单击的x和y像素位置

我在这里列出了一个简化测量的屏幕截图作为例子:

Screenshot with measurements

我最初只是认为你可以用像素宽度划分视野以获得每个像素的度数,但它更复杂,我认为它与投射到球体内部有关,相机位于球体的中心?

如果你能告诉我如何反过来也可以获得奖金......

澄清: 目标不是将视图移动到单击的点,而是提供有关单击点的信息。每像素度数方法不起作用,因为视口不是线性的。

我在这里的值只是示例,但视野可以更大或更小(从[0.something,180],中心不固定,它可以是范围[0, 360°和垂直[-90,90]。点[0,0]只是照片拍摄时照相机的标题(水平度)和音高(垂直度),并不代表任何东西。 / p>

4 个答案:

答案 0 :(得分:12)

TL; DR:本答案末尾包含的概念证明的JavaScript代码。

全景图像的标题和音高参数h0p0对应于方向。通过使用相机的焦距f来缩放此方向向量,可以在(x0, y0, z0)获取视口中心的3D坐标(u0, v0)

x0 = f * cos( p0 ) * sin( h0 )
y0 = f * cos( p0 ) * cos( h0 )
z0 = f * sin( p0 ) 

现在的目标是找到图像中某个给定像素坐标(u, v)处的点的3D坐标。首先,将这些像素坐标映射到视口中心的像素偏移(du, dv)(右侧和顶部):

du = u - u0 = u - w / 2
dv = v0 - v = h / 2 - v

然后必须找到3D中视口的局部正交二维基础。单位向量(ux, uy, uz)支持x轴(沿着增加标题的方向向右),向量(vx, vy, vz)支持y轴(沿着增加的节距方向向上)图片。确定这两个矢量后,视口中与视口中的(du, dv)像素偏移匹配的点的3D坐标就是:

x = x0 + du * ux + dv * vx
y = y0 + du * uy + dv * vy
z = z0 + du * uz + dv * vz

此时标题和音高参数hp是:

R = sqrt( x * x + y * y + z * z )
h = atan2( x, y )
p = asin( z / R )

最后得到两个单位向量(ux, uy, uz)(vx, vy, vz),用(p0, h0)的标题和音高参数计算球坐标的导数,得到:

vx = -sin( p0 ) * sin ( h0 )
vy = -sin( p0 ) * cos ( h0 )
vz =  cos( p0 ) 

ux =  sgn( cos ( p0 ) ) * cos( h0 )
uy = -sgn( cos ( p0 ) ) * sin( h0 )
uz = 0

sgn( a ) +1 a >= 0 -1 f = (w / 2) / Math.tan(fov / 2)

互补物:

  • 焦距来自水平视野和图像宽度:

    (x, y, z)
  • 从航向和俯仰参数到像素坐标的反向映射可以类似地完成:

    1. 找到与指定航向和俯仰参数对应的光线方向的3D坐标(x0, y0, z0)
    2. 找到与视口中心对应的光线方向的3D坐标(x0, y0, z0)(关联的图像平面位于(x0, y0, z0)du正常),
    3. 将指定航向和俯仰参数的光线与图像平面相交,这样就可以从视口中心进行3D偏移,
    4. 在本地基础上投影此3D偏移,获取2D偏移dvdu
    5. dvonLoad()映射到绝对像素坐标。
  • 在实践中,这种方法在方形和矩形视口上的效果似乎相同。

概念证明代码(在包含带有“全景”ID的大小的画布元素的网页上调用'use strict'; var viewer; function onClick(e) { viewer.click(e); } function onLoad() { var element = document.getElementById("panorama"); viewer = new PanoramaViewer(element); viewer.update(); } function PanoramaViewer(element) { this.element = element; this.width = element.width; this.height = element.height; this.pitch = 0; this.heading = 0; element.addEventListener("click", onClick, false); } PanoramaViewer.FOV = 90; PanoramaViewer.prototype.makeUrl = function() { var fov = PanoramaViewer.FOV; return "https://maps.googleapis.com/maps/api/streetview?location=40.457375,-80.009353&size=" + this.width + "x" + this.height + "&fov=" + fov + "&heading=" + this.heading + "&pitch=" + this.pitch; } PanoramaViewer.prototype.update = function() { var element = this.element; element.style.backgroundImage = "url(" + this.makeUrl() + ")"; var width = this.width; var height = this.height; var context = element.getContext('2d'); context.strokeStyle = '#FFFF00'; context.beginPath(); context.moveTo(0, height / 2); context.lineTo(width, height / 2); context.stroke(); context.beginPath(); context.moveTo(width / 2, 0); context.lineTo(width / 2, height); context.stroke(); } function sgn(x) { return x >= 0 ? 1 : -1; } PanoramaViewer.prototype.unmap = function(heading, pitch) { var PI = Math.PI var cos = Math.cos; var sin = Math.sin; var tan = Math.tan; var fov = PanoramaViewer.FOV * PI / 180.0; var width = this.width; var height = this.height; var f = 0.5 * width / tan(0.5 * fov); var h = heading * PI / 180.0; var p = pitch * PI / 180.0; var x = f * cos(p) * sin(h); var y = f * cos(p) * cos(h); var z = f * sin(p); var h0 = this.heading * PI / 180.0; var p0 = this.pitch * PI / 180.0; var x0 = f * cos(p0) * sin(h0); var y0 = f * cos(p0) * cos(h0); var z0 = f * sin(p0); // // Intersect the ray O, v = (x, y, z) // with the plane at M0 of normal n = (x0, y0, z0) // // n . (O + t v - M0) = 0 // t n . v = n . M0 = f^2 // var t = f * f / (x0 * x + y0 * y + z0 * z); var ux = sgn(cos(p0)) * cos(h0); var uy = -sgn(cos(p0)) * sin(h0); var uz = 0; var vx = -sin(p0) * sin(h0); var vy = -sin(p0) * cos(h0); var vz = cos(p0); var x1 = t * x; var y1 = t * y; var z1 = t * z; var dx10 = x1 - x0; var dy10 = y1 - y0; var dz10 = z1 - z0; // Project on the local basis (u, v) at M0 var du = ux * dx10 + uy * dy10 + uz * dz10; var dv = vx * dx10 + vy * dy10 + vz * dz10; return { u: du + width / 2, v: height / 2 - dv, }; } PanoramaViewer.prototype.map = function(u, v) { var PI = Math.PI; var cos = Math.cos; var sin = Math.sin; var tan = Math.tan; var sqrt = Math.sqrt; var atan2 = Math.atan2; var asin = Math.asin; var fov = PanoramaViewer.FOV * PI / 180.0; var width = this.width; var height = this.height; var h0 = this.heading * PI / 180.0; var p0 = this.pitch * PI / 180.0; var f = 0.5 * width / tan(0.5 * fov); var x0 = f * cos(p0) * sin(h0); var y0 = f * cos(p0) * cos(h0); var z0 = f * sin(p0); var du = u - width / 2; var dv = height / 2 - v; var ux = sgn(cos(p0)) * cos(h0); var uy = -sgn(cos(p0)) * sin(h0); var uz = 0; var vx = -sin(p0) * sin(h0); var vy = -sin(p0) * cos(h0); var vz = cos(p0); var x = x0 + du * ux + dv * vx; var y = y0 + du * uy + dv * vy; var z = z0 + du * uz + dv * vz; var R = sqrt(x * x + y * y + z * z); var h = atan2(x, y); var p = asin(z / R); return { heading: h * 180.0 / PI, pitch: p * 180.0 / PI }; } PanoramaViewer.prototype.click = function(e) { var rect = e.target.getBoundingClientRect(); var u = e.clientX - rect.left; var v = e.clientY - rect.top; var uvCoords = this.unmap(this.heading, this.pitch); console.log("current viewport center"); console.log(" heading: " + this.heading); console.log(" pitch: " + this.pitch); console.log(" u: " + uvCoords.u) console.log(" v: " + uvCoords.v); var hpCoords = this.map(u, v); uvCoords = this.unmap(hpCoords.heading, hpCoords.pitch); console.log("click at (" + u + "," + v + ")"); console.log(" heading: " + hpCoords.heading); console.log(" pitch: " + hpCoords.pitch); console.log(" u: " + uvCoords.u); console.log(" v: " + uvCoords.v); this.heading = hpCoords.heading; this.pitch = hpCoords.pitch; this.update(); } 函数)

{{1}}

答案 1 :(得分:4)

这个答案很准确,请查看user3146587的最新答案。

我不擅长数学解释。我编写了一个示例并尝试解释代码中的步骤。单击图像中的一个点后,此点将成为图像的新中心。尽管你明确没有要求这个,但这对于说明效果是完美的。使用先前计算的角度绘制新图像。

示例:JSFiddle

重要的是,我使用radian来计算“视野”的半径。在这种情况下,弧度是图像的宽度(在您的示例中为100)

radius = radian / FOV

使用弧度,半径和鼠标位置的相对位置,我可以计算从中心位置变化到鼠标位置的程度。

Center(50,50)
MousePosition(75/25)
RelativeMousePosition(25,-25)

当相对鼠标位置为25时,用于计算水平角度的弧度为50。

radius = 50 / FOV // we've calculated the radius before, it stays the same

有关进一步的流程,请参阅此图片: enter image description here

当我将计算出的角度加到/减去实际角度时(取决于左/右,上/下),我可以计算新的航向和俯仰。请参阅链接的JSFiddle,了解其正确行为。

反过来很简单,只需按相反方向执行列出的步骤(半径保持不变)。

正如我已经提到的,我不擅长数学解释,但在评论中不要犹豫。

答案 2 :(得分:1)

这是尝试对您的问题的答案进行数学推导。

注意:不幸的是,这种推导仅适用于1D,并且从一对角度偏差到航向和俯仰的转换是错误的。

符号:

f:相机的焦距
h:视口的高度(以像素为单位) w:视口的宽度(以像素为单位) dy:与视口中心的垂直偏差(以像素为单位) dx:与视口中心的像素水平偏差
fov_y:垂直视野

fov_x:水平视野

dtheta_y:距视口中心的相对垂直角度 dtheta_x:距视口中心的相对水平角度

给定dy,像素距视口中心的垂直偏移(此像素对应于图中的绿色光线),我们试图找到dtheta_y(红色角度) ,距视口中心的相对垂直角度(视口中心的间距已知为theta_y0)。

Notations

从图中我们得到:

tan( fov_y / 2 ) = ( h / 2 ) / f

tan( dtheta_y ) = dy / f

这样:

tan( dtheta_y ) = dy / ( ( h / 2 ) / tan( fov_y / 2 ) )
                = 2 * dy * tan( fov_y /  2 ) / h

最后:

dtheta_y = atan( 2 * dy * tan( fov_y / 2 ) / h )

这是距视口中心dy处像素的相对俯仰角,只需在视口中心添加俯仰角即可获得绝对俯仰角(即{{1} })。

类似地:

theta_y = theta_y0 + dtheta_y

这是距视口中心dtheta_x = atan( 2 * dx * tan( fov_x / 2 ) / w ) 处像素的相对航向角。

互补物:

  • 两个关系都可以反转,以获得从相对航向/俯仰角到相对像素坐标的映射,例如:

    dx
  • 垂直和水平视野dy = h tan( dtheta_y ) / ( 2 * tan( fov_y / 2 ) ) fov_y按关系链接:

    fov_x

    这样:

    w / h = tan( fov_x / 2 ) / tan( fov_y / 2 )
    
  • 视口中心fov_x = 2 * atan( w * tan( fov_y / 2 ) / h ) dy的垂直和水平偏差可以映射到绝对像素坐标:

    dx
  • 概念证明fiddle

答案 3 :(得分:0)

Martin Matysiak写了一个JS库来实现这个的反向(在特定的标题/音高上放置一个标记)。我提到这个,因为其他答案中的各种jsfiddle链接是404ing,原始请求者添加了一个请求这个的评论,并且这个SO页面在相关搜索的顶部附近出现。

讨论它的博客文章位于https://martinmatysiak.de/blog/view/panomarker

图书馆本身位于https://github.com/marmat/google-maps-api-addons

http://marmat.github.io/google-maps-api-addons/上的文档和演示(请查看http://marmat.github.io/google-maps-api-addons/panomarker/examples/basic.htmlhttp://marmat.github.io/google-maps-api-addons/panomarker/examples/fancy.html了解PanoMarker示例。)