Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setBearing #278

Open
andrewharvey opened this issue Jun 13, 2018 · 5 comments
Open

setBearing #278

andrewharvey opened this issue Jun 13, 2018 · 5 comments

Comments

@andrewharvey
Copy link

This might unlock #156. At the moment we can moveCloseTo, but especially for 360 images we might need to set the bearing so that the image is looking at a given lat,lon.

@cbeddow
Copy link
Member

cbeddow commented Jun 13, 2018

I think the best way to do this is a combination of API and MapillaryJS.

For the API, you'll want to take the coordinates you want to move close to, and make an API call using closeto and lookat both. This will find images close to, but with the camera angle oriented toward, the point you indicate.

Here is an example:

https://a.mapillary.com/v3/images?client_id=YOUR_CLIENT_ID&closeto=-122.95132398605347,45.52770459072315&radius=40&lookat=-122.95132398605347,45.52770459072315

This uses the same coordinates for both, and has a radius of 40 meters--default radius is 100 meters. You can also put two different sets of coordinates, for example you may want images on the west side of a river but looking at a point on the east side. The closeto parameter is always required, lookat is an optional add-on.

The API response will show the "best" image first, typically that is most directly in the camera angle's field of view, and nearest to the closeto point. Sometimes a building or vegetation is in the way, so be aware that the first isn't always the best.

You will then grab the image key from the API response, and have the viewer call moveToKey. If the camera angle is still off, then you'll need to get the coordinates of current image (node.latLon), then do an external calculation to see the bearing from image to the point of interest, then we would theoretically need a setBearing function. Currently we have setCenter but I'm not sure if there's a clear way to convert a bearing into XY coordinates for that to consume.

@andrewharvey
Copy link
Author

This is mostly an issue with 360 imagery, as this standard approach above works pretty well for regular images, but breaks down with 360.

You will then grab the image key from the API response, and have the viewer call moveToKey. If the camera angle is still off, then you'll need to get the coordinates of current image (node.latLon), then do an external calculation to see the bearing from image to the point of interest, then we would theoretically need a setBearing function. Currently we have setCenter but I'm not sure if there's a clear way to convert a bearing into XY coordinates for that to consume.

Yep, that's exactly what I did, grabbing node.latLon and calculated the bearing, but without a setBearing method that approach is stalled. Open to other options in the API, if setBearing isn't necessarily the best way.

@oscarlorentzon
Copy link
Member

oscarlorentzon commented Jun 14, 2018

@andrewharvey Thanks for the suggestion.

When implementing Viewer.setCenter in MapillaryJS 1.7.0 we considered adding a setBearing method to the Viewer API. We opted against it for two reasons:

  1. The API would not be consistent across image types. While setBearing makes sense for full 360 panoramas is not not useful for regular image where the horizontal field of view is generally between 60 and 120 degrees.
  2. The API would not be consistent over time. When the Structure from Motion is run/rerun on an area (e.g. when new images are uploaded in that area) the image positions and rotations are shaken more or less. If we set the bearing on a specific node/panorama at a certain point in time we are looking at a certain coordinate in the image. If SfM is rerun and we set the same bearing at a later point in time we will be looking at another coordinate in the image.

We do not plan to add a setBearing API at the moment because of those considerations. Although, it is fairly straight forward to set the bearing for full 360 panoramas with what is exposed in the API currently. The second consideration above applies here of course. See the example code below:

var mly = new Mapillary.Viewer(
    'mly',
    // Replace this with your own client ID from mapillary.com
    '<your-client-id>',
    null);

mly.moveToKey('Q1PJAgFZcr1iFgndBGcVaQ').then(
    function(node) { setBearing(node); },
    function(error) { console.error(error); });

function setBearing(node) {
    if (!node.fullPano) {
        // We are only interested in setting the bearing for full 360 panoramas.
        return;
    }

    var nodeBearing = node.computedCA; // Computed node compass angle (equivalent
                                        // to bearing) is used by mjs when placing
                                        // the node in 3D space.
    var desiredBearing = 90; // Your desired bearing.

    var basicX = bearingToBasic(desiredBearing, nodeBearing);
    var basicY = 0.5; // Vertical center

    var center = [basicX, basicY];

    mly.setCenter(center);
}

/**
 * Convert a desired bearing to a basic X image coordinate for
 * a specific node bearing.
 *
 * Works only for a full 360 panorama.
 */
function bearingToBasic(desiredBearing, nodeBearing) {

    // 1. Take difference of desired bearing and node bearing in degrees.
    // 2. Scale to basic coordinates.
    // 3. Add 0.5 because node bearing corresponds to the center
    //    of the image. See
    //    https://mapillary.github.io/mapillary-js/classes/viewer.html
    //    for explanation of the basic coordinate system of an image.
    var basic = (desiredBearing - nodeBearing) / 360 + 0.5;

    // Wrap to a valid basic coordinate (on the [0, 1] interval).
    // Needed when difference between desired bearing and node
    // bearing is more than 180 degrees.
    return wrap(basic, 0, 1);
}

/**
 * Wrap a value on the interval [min, max].
 */
function wrap(value, min, max) {
    var interval = (max - min);

    while (value > max || value < min) {
        if (value > max) {
            value = value - interval;
        } else if (value < min) {
            value = value + interval;
        }
    }

    return value;
}

// Resize the viewer when the window is resized.
window.addEventListener("resize", function() { mly.resize(); });

There is some extensive commenting in the code which makes it look a bit more verbose than it actually is. Let me know if you have any questions and whether this resolves your use case or not.

@andrewharvey
Copy link
Author

Thanks for the code @oscarlorentzon, it works perfectly!

Agreed this is only really useful for 360, but I think it would be nice to wrap it up so people building apps have a simpler interface, it's a lot of code at the moment and it's at a level deeper than I'd like. I'd even go one step higher and have a lookAt method, which given a lat,lon will automatically set the bearing based on the current nodes lat,lon.

I'm not sure I get point 2:

The API would not be consistent over time. When the Structure from Motion is run/rerun on an area (e.g. when new images are uploaded in that area) the image positions and rotations are shaken more or less. If we set the bearing on a specific node/panorama at a certain point in time we are looking at a certain coordinate in the image. If SfM is rerun and we set the same bearing at a later point in time we will be looking at another coordinate in the image.

Does that matter? The final coordinates passed to setCenter are internal details, so it doesn't matter if next time they make the call they are different.

@oscarlorentzon
Copy link
Member

Point 2 is relevant when one wants to be able to share a specific viewpoint of the image that is consistent over time. That for example applies to sharing a link like https://www.mapillary.com/app/?lat=55.60292981583336&lng=13.013955345277736&z=17&pKey=_q5-pCxe74HohcPQiXXPZw&x=0.2727110367181517&y=0.4809297983683465&zoom=1.1795239380556855&focus=photo where the basicX, basicY and zoom is persisted in the URL. Agree that it is not as relevant in your case.

Also agree that it is a lot of code and quite deep. The lookAt solution you mention is interesting for the future. It also involves resolving the #243 equivalent for lookAt if only 360 panos are desired from the lookAt result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants