Получить заголовок и шаг из пикселей в режиме просмотра улиц

Я думаю, что это лучшее место для этого вопроса.

Я пытаюсь получить заголовок и шаг любой точки щелчка на встроенном Просмотре улиц Google.

Единственными частями информации, которые я знаю и могу получить, являются:

  • Поле зрения (в градусах)
  • Заголовок центральной точки и шаг (в градусах) и положение пикселя x и y
  • Положение пикселя x и y щелчка мыши

Здесь я привел скриншот с упрощенными измерениями:

Screenshot with measurements

Я только начинал думать, что вы можете разделить поле зрения на ширину пикселя, чтобы получить градусы на пиксель, но это сложнее, я думаю, что это связано с проецированием на внутренность сферы, где камера находится на центр сферы?

Бонус, если вы можете сказать мне, как сделать обратное тоже...

Разъяснение: Цель состоит не в том, чтобы переместить представление в точку щелчка, а в том, чтобы предоставить информацию о щелчке. Метод градусов на пиксель не работает, поскольку окно просмотра не является линейным.

Значения, которые я имею здесь, являются просто примерами, но поле зрения может быть больше или меньше (из [0.something, 180], а центр не фиксирован, это может быть любое значение в диапазоне [0, 360] и вертикально [-90, 90]. Точка [0, 0] - это просто заголовок (горизонтальные градусы) и шаг (вертикальные градусы) фотогамера, когда фотография была сделана, и на самом деле ничего не представляет.

Ответы

Ответ 1

TL; DR: код JavaScript для доказательства концепции, включенный в конце этого ответа.

Параметры заголовка и основного тона h0 и p0 панорамного изображения соответствуют направлению. Используя фокусное расстояние f камеры для масштабирования этого вектора направления, можно получить 3D-координаты (x0, y0, z0) центра просмотра в (u0, v0):

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

Цель состоит в том, чтобы теперь найти 3D-координаты точки в некоторых заданных координатах пикселя (u, v) на изображении. Сначала сопоставьте эти пиксельные координаты смещениям пикселей (du, dv) (справа и вверху) из центра просмотра:

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

Тогда должен быть найден локальный ортонормированный двумерный базис в окне просмотра в 3D. Единичный вектор (ux, uy, uz) поддерживает ось x (справа вдоль направления увеличения заголовков), а вектор (vx, vy, vz) поддерживает ось y (вверху по направлению увеличения высоты) изображения. Как только эти два вектора определены, 3D-координаты точки в окне просмотра, соответствующие смещению пикселя (du, dv) в окне просмотра, просто:

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

где sgn( a ) есть +1, если a >= 0 else -1.

Дополняет:

  • Фокусное расстояние получается из горизонтального поля зрения и ширины изображения:

    f = (w / 2) / Math.tan(fov / 2)
    
  • Обратное сопоставление от заголовков и параметров основного тона к пиксельным координатам может быть выполнено аналогичным образом:

    • Найдите 3D-координаты (x, y, z) направления луча, соответствующего указанным параметрам заголовка и тангажа,
    • Найдите 3D-координаты (x0, y0, z0) направления луча, соответствующего центру окна просмотра (соответствующая плоскость изображения находится в (x0, y0, z0) с нормалью (x0, y0, z0)),
    • Пересечение луча для заданных параметров заголовка и тангажа с помощью плоскости изображения, это дает смещение 3D из центра просмотра,
    • Спроектируйте это трехмерное смещение на локальном уровне, получив двумерные смещения du и dv
    • Карта du и dv в абсолютные пиксельные координаты.
  • На практике этот подход, похоже, работает одинаково хорошо как в квадратных, так и в прямоугольных видовых экранах.

Доказательство кода концепции (вызов функции 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();
}

Ответ 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

Посмотрите это изображение для дальнейшего процесса: enter image description here

Я могу рассчитать новый заголовок и шаг, когда я добавляю/вычитаю вычисленный угол в фактический угол (в зависимости от левого/правого, выше/ниже). См. Связанный JSFiddle для правильного поведения этого.

Делать обратное просто, просто выполните перечисленные шаги в противоположном направлении (радиус остается тем же).

Как я уже говорил, я не очень разбираюсь в математических объяснениях, но не стесняйтесь задавать вопросы в комментариях.

Ответ 3

Вот попытка дать математический вывод ответа на ваш вопрос.

Примечание. К сожалению, этот вывод работает только в 1D, а преобразование из пары отклонений angular к заголовку и тангажу неверно.

Обозначения:

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_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

Ответ 4

Мартин Матисяк написал 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.html и http://marmat.github.io/google-maps-api-addons/panomarker/examples/fancy.html для примеров PanoMarker).