Procedural Data Provider
You can use the data provider API to provide data in the MapillaryJS ent format. The data can come from anywhere, e.g. service APIs, JSON files, or even be generated on the fly in the browser. In this guide we will do procedural data generation for simplicity.
You will learn
- How to convert your data to compatible MapillaryJS contracts and ents
- How to create your own provider by extending the
DataProviderBase
class - How to make the
Viewer
use your data provider to load data
#
OverviewWe will soon go into the details of our data provider, but first we will get an overview of the data provider concept. The data provider overview below explains how the data provider API works, step by step.
Data provider overview
The data provider API works like this:
- We start at the bottom with our own data format. We want to use it in MapillaryJS.
- We determine a way of serving our data to the provider. This could be through a web service, through file IO dialogs, or any other way that suites our use case. In our procedural data provider, we will generate the data on the fly in the browser.
- To consume the data we write our own data provider class which is responsible for data loading and conversion to the MapillaryJS ent format.
- When creating the MapillaryJS viewer, we supply our provider instance as an option.
- The default data provider is overridden and our own provider is used instead.
- When MapillaryJS makes requests, our provider will make all the decisions about how to retrieve the data and how to convert it.
Now, let's dig a bit deeper.
#
Image GenerationMapillaryJS depends on images being provided for each camera capture in the data. Therefore we begin by generating images to have a foundation. We will provide images of mango colored grids because of their nice visualization properies.
SyntaxError: Unexpected token (1:8) 1 : return () ^
note
If your data does not contain any images, you can generate image buffers consisting of a single pixel and provide them to MapillaryJS. See the OpenSfM visualization tool for a data provider using that as a fallback strategy.
#
Contracts and EntsThe most important ent for the data provider is the ImageEnt. Each camera capture is represented as an image ent in the provider. While some image ent properties are self explanatory, we will go through a few that are a bit more involved. While many other contracts and ents need to be served from the provider, they are not as complex so we refer to the API reference for information about those.
#
Camera Type and ParametersMapillaryJS supports three camera types. They map directly to the camera types defined and used in OpenSfM.
Camera Type | Camera Parameters |
---|---|
Perspective | [focal, k1, k2] |
Fisheye | [focal, k1, k2] |
Spherical | [] (No parameters) |
The order of the camera parameters must be retained. Any other camera type will be treated as perspective.
#
Exif OrientationDescribes the rotation of the camera capture.
#
Computed RotationInternally, MapillaryJS operates in a local topocentric East, North, Up (ENU) reference frame.
If your data is transformed into another coordinate system, you need to apply the inverse of that transform to the angle-axis representation for each camera capture.
In our procedural provider we want all cameras to look to north, so we use the [Math.PI / 2, 0, 0]
angle-axis rotation for the camera reference to world reference frame transform.
#
Computed vs OriginalWhen running a Structure from Motion algorithm, EXIF GPS positions and other metadata are often used as priors. The algorithm will then improved the positioning of the camera captures. Properties prefixed with computed
in MapillaryJS refer to output from an algorithm. Properties prefixed original
refer to metadata from the capture device. If you do not have original metadata, you can just set it to the computed value.
#
Merge IDThe merge ID informs MapillaryJS what camera captures should be treated as a connected component and therefore have smooth transitions between them. In our example, we give all cameras the same merge ID.
#
Mesh, Thumb and ClusterAll the nested object used by MapillaryJS to request additional data are URLEnts.
const thumb = {id: '<my-thumb-id', url: '<my-thumb-url>'};
When requesting the data from the provider, MapillaryJS will only provide the URL as parameter and the provider needs to act based on that. MapillaryJS expects JSON objects and array buffers to be returned from the data provider and exposes the decompress, fetchArrayBuffer, and readMeshPbf functions for convenience.
Meshes are provided per camera capture with coordinates in the camera reference frame.
#
ConversionYour data format may not have exactly the same property structure that MapillaryJS expects. In that case you can implement a converter to convert the data before providing it to MapillaryJS.
#
Camera Capture GenerationWe want to visualize all the three camera types that MapillaryJS supports with camera parameters specified like so.
const cameraTypes = [ { cameraType: 'fisheye', focal: 0.45, k1: -0.006, k2: 0.004, }, { cameraType: 'perspective', focal: 0.8, k1: -0.13, k2: 0.07, }, { cameraType: 'spherical', },];
#
Implementing the ProviderNow that we have generated the data, we need to extend the abstract DataProviderBase class.
tip
When debugging your data provider, it is a good idea to call the methods directly to ensure that they return correctly converted ents and contracts before attaching it to the Viewer.
constructor
#
We populate the generated data in the constructor. Here we also generate geometry cells mapping a geo cell to an array of images.
class ProceduralDataProvider extends DataProviderBase { constructor(options) { super(options.geometry ?? new S2GeometryProvider());
const {images, sequences} = generateImages(); this.images = images; this.sequences = sequences; this.cells = generateCells(images, this._geometry); }
getCluster
#
We do not generate any point clouds for this example so we return empty clusters.
class ProceduralDataProvider extends DataProviderBase { // ... getCluster(url) { return Promise.resolve({points: {}, reference: REFERENCE}); }}
For this example, we return the complete image contract because we already have the generated data. For bandwidth, latency, and performance reasons in production, it is recommended to only request the CoreImageEnt properties from the service.
getCoreImages
#
class ProceduralDataProvider extends DataProviderBase { // ... getCoreImages(cellId) { const images = this.cells.has(cellId) ? this.cells.get(cellId) : []; return Promise.resolve({cell_id: cellId, images}); }}
We generate our mango image buffer on the fly.
getImageBuffer
#
class ProceduralDataProvider extends DataProviderBase { // ... getImageBuffer(url) { return generateImageBuffer(); }}
getImages
#
If an image has been generated, we return it as a node contract.
class ProceduralDataProvider extends DataProviderBase { // ... getImages(imageIds) { const images = imageIds.map((id) => ({ node: this.images.has(id) ? this.images.get(id) : null, node_id: id, })); return Promise.resolve(images); }}
getImageTiles
#
We will deactivate the image tiling functionality with a viewer option so we do not need to implement this method (we can omit this code completely).
class ProceduralDataProvider extends DataProviderBase { // ... getImageTiles(tiles) { return Promise.reject(new MapillaryError('Not implemented')); }}
getMesh
#
We do not generate any triangles for this example so we return empty meshes.
class ProceduralDataProvider extends DataProviderBase { // ... getMesh(url) { return Promise.resolve({faces: [], vertices: []}); }}
getSequence
#
If a sequence has been generated, we return it.
class ProceduralDataProvider extends DataProviderBase { // ... getSequence(sequenceId) { return new Promise((resolve, reject) => { if (this.sequences.has(sequenceId)) { resolve(this.sequences.get(sequenceId)); } else { reject(new Error(`Sequence ${sequenceId} does not exist`)); } }); }}
getSpatialImages
#
We reuse the previously implemented getImages
method.
class ProceduralDataProvider extends DataProviderBase { // ... getSpatialImages(imageIds) { return this.getImages(imageIds); }}
#
Attaching the ProviderNow that we have implemented our procedural data provider, we just need to attach it to the Viewer through the ViewerOptions.dataProvider option. Lastly, we need to move to one of the image ids in our generated data to initialize the Viewer.
tip
Try changing some of the viewer options, e.g. setting the camera controls to Street
to get another perspective.
SyntaxError: Unexpected token (1:8) 1 : return () ^
#
RecapNow you know how to provide MapillaryJS with your own data by:
- Extending the
DataProviderBase
class and implementing its abstract methods - Convert your data to the MapillaryJS ent format
- Attach your custom data provider to the MapillaryJS viewer
info
You can view the complete code in the Procedural Data Provider example.
If you want to build a data provider fetching files from a server, you can use the OpenSfM data provider as inspiration.