从街景像素中获取航向和俯仰角

15

我认为这是询问此问题的最佳场所。

我正在尝试获取嵌入式Google Street View上任何点击点的标题和角度。

我知道并且可以获取的唯一信息是:

  • 视野(度数)
  • 中心点的标题和角度(以度为单位)、X和Y像素位置
  • 鼠标单击的X和Y像素位置

我在这里包括了一个具有简化测量的屏幕截图示例:

屏幕截图,带有测量值

我最初只是认为您可以将视野除以像素宽度来获得每个像素的角度,但这更加复杂,我认为与投影到球体内部有关,其中相机位于球体的中心?

如何反向操作会更好...

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

这里的值仅为示例,但视野可以更大或更小(从[0.某些事情, 180]),中心不是固定的,它可以是范围内的任何值[0, 360]和垂直[-90, 90]。点[0,0]只是摄影师拍摄照片时的标题(水平度数)和俯仰(垂直度数),并不真正代表任何东西。


这个问题也许更适合在 StackOverflow 上发问?如果是的话,能否有人帮我转移一下? - Tim Rodham
什么是反对每像素度数的想法?即使它会产生一些错误,不断地这样做也会让你达到想要达到的目标,但问题也不是很清楚,$0^o, 0^o$ 是水平向前吗?(那么它不应该靠近地平线,或者道路是下坡的,还是针对飞机?)而 $90^o, 0^o$ 是向右看吗?这一切都有点不清楚,你能添加更多细节吗? - Willemien
@Willemien 谢谢,我已经对问题进行了澄清。 - Tim Rodham
视图是否能够达到完全的180度?我正在考虑一种解决方案,它会将点击点绘制在弦上,从原点到该点绘制一条线,然后将该线延伸到弧上。这个解决方案可能有效,但如果lengthOfChord == diameter(即180°角),它就会失效。 - Tyler Eich
是的,它可以准确地达到180,但如果您需要近似值,您可以在这种特定情况下将其设置为180.00001。 - Tim Rodham
4个回答

13

简而言之,本文末尾包含一个用于概念验证的JavaScript代码。

全景图像的标题和俯仰参数 h0p0 对应于一个方向。通过使用相机的焦距 f 来缩放这个方向向量,可以得到视口中心在 (u0, v0) 处的 3D 坐标 (x0, y0, z0)

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

现在的目标是找到图像中某个给定像素坐标对应的点的三维坐标。首先,将这些像素坐标映射到相对于视口中心的像素偏移量(向右和向上)。
du = u - u0 = u - w / 2
dv = v0 - v = h / 2 - v

首先需要找到在3D中视口的本地二维正交基。单位向量(ux, uy, uz)沿着x轴(随着方位角的增加向右)支持图像,向量(vx, vy, vz)沿着y轴(随着俯仰角的增加向上)支持图像。一旦确定了这两个向量,视口中与(du, dv)像素偏移匹配的点的三维坐标就很简单了:

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 )a >= 0 时为 +1,否则为 -1

补码:

  • The focal length is derived from the horizontal field of view and the width of the image:

    f = (w / 2) / Math.tan(fov / 2)
    
  • The reverse mapping from heading and pitch parameters to pixel coordinates can be done similarly:

    1. Find the 3D coordinates (x, y, z) of the direction of the ray corresponding to the specified heading and pitch parameters,
    2. Find the 3D coordinates (x0, y0, z0) of the direction of the ray corresponding to the viewport center (an associated image plane is located at (x0, y0, z0) with an (x0, y0, z0) normal),
    3. Intersect the ray for the specified heading and pitch parameters with the image plane, this gives the 3D offset from the viewport center,
    4. Project this 3D offset on the local basis, getting the 2D offsets du and dv
    5. Map du and dv to absolute pixel coordinates.
  • In practice, this approach seems to work similarly well on both square and rectangular viewports.

概念验证代码(在包含带有“全景”ID的大小画布元素的网页上调用onLoad()函数)

'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();
}

谢谢,太棒了 - 没有任何办法可以让这个适用于矩形视口吗? - Tim Rodham
@TimRodham 我猜应该可以将其适应于非正方形视口。不幸的是,我要赶上一次跨大西洋的飞行,所以这不会很快发生... - user3146587
无论如何,感谢您的帮助,我会给您悬赏,因为这是最接近我所需的。 - Tim Rodham
1
@TimRodham 更新了反向映射。我仍然需要弄清楚非正方形视口的情况 :-) - user3146587
1
@Geekster 我添加了一些 PoC JS 代码,因为 jsfiddle 链接已过期。除非我错了,只要 FoV 推导正确,该方法应该适用于矩形视口... 在实践中,在正方形和矩形视口上玩了一会儿后,我得到了类似的结果,所以我相应地修改了评论... - user3146587
显示剩余5条评论

4

这个答案不够精确,请看用户3146587的最新答案。

我不太擅长数学解释。我编写了一个示例并尝试解释代码中的步骤。当您单击图像中的某一点时,该点将成为图像的新中心。即使您没有明确要求此功能,但它非常适合说明效果。使用先前计算出的角度绘制新图像。

示例:JSFiddle

重要的部分是我使用弧度来计算“视野范围”的半径。在这种情况下,弧度是图像的宽度(在您的示例中为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以获得正确的行为。
相反的操作很简单,只需按相反方向执行列出的步骤(半径保持不变)。
正如我已经提到的,我并不擅长数学解释,但是请在评论中随时提问。

感谢您抽出时间回答,不幸的是,您提供的代码示例并不完全准确 - 如果您点击建筑物的角落,您期望图像以该角落为中心,但实际上它会稍微偏移? - Tim Rodham

1
这里尝试给出一个数学推导,回答您的问题。
注意:不幸的是,这个推导只适用于一维情况,并且从一对角度偏差转换到航向和俯仰角是错误的。
符号说明:
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的像素的相对俯仰角,只需将其加到视口中心的俯仰角上即可得到绝对俯仰角(即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_yfov_x由以下关系链接:

    w / h = tan( fov_x / 2 ) / tan( fov_y / 2 )
    

    所以:

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

    x = w / 2 + dx
    y = h / 2 - dy
    
  • 概念验证fiddle


谢谢您的回答,看起来很不错,但实际上并不准确。我想知道街景是否没有使用球体...您介意编写一个小例子来确保我已经正确地将其转换为代码,而没有犯傻瓜错误吗? - Tim Rodham
@TimRodham 我刚刚添加了一个概念证明。 - user3146587
太好了,谢谢。不幸的是,这与其他答案的问题相同,似乎存在精度问题。请看这里 - 我在图像中心添加了一个红色div。如果您单击地标,您会发现它们有时与中心偏离 - http://jsfiddle.net/T9Yc5/2/ - Tim Rodham
@TimRodham 确实,可以看看我的另一个答案,这个似乎符合你的期望。 - user3146587

0

Martin Matysiak编写了一个JS库,实现了这个的反向操作(在特定标题/俯仰处放置标记)。我提到这一点是因为其他答案中的各种jsfiddle链接都已经404了,原始请求者添加了一条评论请求此内容,并且这个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示例)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接