【问题标题】:Get heading and pitch from pixels on Street View从街景上的像素获取航向和俯仰
【发布时间】:2014-02-05 10:15:48
【问题描述】:

我认为这是回答这个问题的最佳地点。

我正在尝试获取嵌入式 Google 街景视图上任何点击点的航向和间距。

我知道并能得到的信息只有:

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

我在这里包含了一个带有简化测量的屏幕截图作为示例:

我最初只是认为您可以将视野除以像素宽度以获得每个像素的度数,但它更复杂,我认为它与投影到球体内部有关,即相机所在的位置球心?

如果你也能告诉我如何做相反的事情,那么奖励......

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

我这里的值只是例子,但是视野可以更大或更小(从[0.something,180],并且中心不固定,它可以是[0,范围内的任何值, 360] 和垂直 [-90, 90]。点 [0, 0] 只是拍照时摄影机的航向(水平度数)和俯仰(垂直度数),并不能真正代表任何东西。

【问题讨论】:

  • 这个问题对 StackOverflow 来说更好吗?如果是这样,有人可以帮我移动它吗?
  • 每像素度数的想法是什么?即使它给出了一些错误,不断地这样做也会让你达到你想要达到的程度,但问题也不是很清楚,是 $ 0^o , 0^o $ 水平前面吗? (那么它不应该靠近地平线,还是下坡路,还是飞机?) $ 90^o , 0^o $ 看起来对吗?有点不清楚,您可以添加更多详细信息
  • @Willemien 谢谢,我已经对问题进行了澄清。
  • 视野能否达到精确 180°?我正在考虑一种解决方案,该解决方案采用单击的点,将其绘制在弦上,从原点到该点画一条线,然后将这条线延续到弧上。这个解决方案可以工作,但如果 lengthOfChord == diameter(即 180° 角),它就会崩溃。
  • 是的,它可以恰好达到 180,但如果您需要近似,在这种特定情况下,您可以将其设置为 180.00001

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


【解决方案1】:

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

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

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 视口的局部正交 2D 基础。单位向量(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

那么此时的航向和俯仰参数h 和p 是:

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

如果a >= 0 则sgn( a ) 是+1 否则-1。

补充:

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

    f = (w / 2) / Math.tan(fov / 2)
    
  • 从航向和俯仰参数到像素坐标的反向映射可以类似地完成:

    1. 找到指定航向和俯仰参数对应的射线方向的3D坐标(x, y, z),
    2. 找到与视口中心相对应的射线方向的 3D 坐标 (x0, y0, z0)(关联的图像平面位于 (x0, y0, z0),法线为 (x0, y0, z0)),
    3. 将指定航向和俯仰参数的射线与图像平面相交,这给出了与视口中心的 3D 偏移,
    4. 在本地投影这个 3D 偏移,得到 2D 偏移du 和 dv
    5. 将du 和dv 映射到绝对像素坐标。
  • 在实践中,这种方法似乎在方形和矩形视口上都同样适用。

概念证明代码(在包含具有“全景”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();
}

【讨论】:

  • 谢谢,这太棒了 - 有没有办法让它适用于矩形视口?
  • @TimRodham 我想这应该可以适应非方形视口。不幸的是,我要赶跨大西洋的航班,所以这不会很快发生......
  • 谢谢你的帮助,我给你赏金,因为这是最接近我需要的东西
  • @TimRodham 更新了反向映射。我仍然需要弄清楚非方形视口:-)
  • @Geekster 我添加了一些 PoC JS 代码,因为 jsfiddle 链接已过期。除非我弄错了,只要 FoV 推导正确,该方法应该适用于矩形视口......实际上,在正方形和矩形视口上玩了一下之后,我得到了相似的结果,所以我相应地修改了评论...
【解决方案2】:

此答案不准确,请查看 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

查看此图片了解进一步的流程:

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

反向操作很简单,只需沿相反方向执行所列步骤即可(半径保持不变)。

正如我已经提到的,我不太擅长数学解释,但请不要犹豫,在 cmets 中提问。

【讨论】:

  • 感谢您抽出宝贵时间回答,不幸的是,您给出的小提琴不太准确 - 例如,如果您单击建筑物的角落,您会希望图像居中在那个拐角处,但稍微偏移了一点?
【解决方案3】:

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

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

符号:

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

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

从图中我们有:

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_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的垂直和水平偏差可以映射到绝对像素坐标:

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

【讨论】:

  • 感谢您的回答,这看起来很可靠,但不幸的是在实践中它并不准确。我想知道街景是否没有使用球体...您介意编写一个小提琴以确保我已将其正确翻译成代码并且没有犯愚蠢的错误吗?
  • @TimRodham 我刚刚添加了一个概念证明。
  • 非常感谢,不幸的是,它与其他答案的问题相同,似乎存在准确性问题。看这里 - 我在图像的中心添加了一个红色 div。如果您点击地标,您有时会看到它们偏离中心 - jsfiddle.net/T9Yc5/2
  • @TimRodham 确实,请参阅我的另一个答案,这个答案似乎可以按您的预期工作。
【解决方案4】:

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.html 和http://marmat.github.io/google-maps-api-addons/panomarker/examples/fancy.html 以获取 PanoMarker 示例)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    • 2019-01-15
    • 2011-05-18
    • 2020-08-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多