Invisible Map Generation

On this page

Overview

The backend is the halfway point between Invisible Map Creator and Invisible Map. It will pull the unprocessed maps from Firebase, optimize it, identify any new connections to be made, then give it back to firebase with the new data. It is written primarily in Python, with some supporting C code that should be abstracted away for the most part. The whole thing is run on a server that Paul has access to, which will run a script (process_graphs) whenever it detects a new map from Firebase. The pipeline is as follows:

  1. FirebaseManager is notified of an update to Firebase, and downloads the map before handing it off to GraphManager
  2. as_graph converts the raw JSON to a Graph object
  3. This Graph can then convert itself to a SparseOptimizer, directly from the g2o library
  4. GraphManager handles the optimization through the SparseOptimizer, pulling the new odometry and tag poses from the optimizer
  5. graph_utils detects any intersections, giving the new list of neighbors to GraphManager
  6. GraphManager compiles the new poses and neighbors into a dict
  7. That dict is given to FirebaseManager to reupload so InvisibleMap can see it

Additionally, there are a few extra scripts for us to run manually:

  1. graph_manager_user: manually download and process maps
  2. correlate_metrics: determines the correlation between different metrics of ranking weights
  3. optimize_weights: either runs a genetic algorithm or a sweep to determine the best set of weights to use
  4. visualize_chi2: helps to see where error from chi2s are coming from

Graph

Graph is the main data structure the backend uses to contain any and all information about the map the user made. For the most part, it contains a list of type Vertex and a list of type Edge, along with a ton of functions for optimization, converting to a SparseOptimizer, and some utility functions to aid certain metrics.

Vertex

Vertex is a very basic wrapper class to represent a single vertex of a Graph. It has four attributes:

In Graph, the vertices are stored in a dict of int to Vertex. Each key is a uid for the Vertex. While typically assigned in order (i.e. the dict is essentially a list), this is not necessarily the case, so make sure to watch out for this! Also note that these need to be converted to G2O’s own Vertex type before optimization; typically a VertexSE3Expmap 1.

Edge

Edge is another wrapper class for Graph, containing the basic information needed to represent a connection between two vertices. Its attributes are:

Like Vertex, Edges are stored as a dict of int to Edge. In general, we refer to edges as “odometry edges”, “tag edges”, etc. This refers to the type of node it connects. Because of the way our mapping is set up, all edges are connected to at least one odometry node. This will always be the start node. Therefore, “odometry edge” is shorthand for “odometry-to-odometry edge” and “tag edge” is short for “odometry-to-tag edge”, etc. Note also that these are only our own edge types; when converting to G2O, these need to be converted to their own edge types. Most of the time, this will be EdgeSE3Expmap 2.

Other Attributes

There are a few other items Graph will store. These include:

Methods

Update Graph

Chi Squared and Evaluation

Optimization

While there are many other methods in Graph, the rest do not need to be rigorously explained. They are either utility methods, not typically called outside of Graph, or relate to weight tuning ("expectation maximization"), which is not important for now.

As Graph

as_graph essentially contains just one function: converting the raw JSON uploaded from Invisible Map Creator into a Graph object. While the code is long and looks complicated, most of it is simply setup for the final part: creating data structures for more efficient lookup of information. After pulling all of the edge and vertex information into these various structures, it loops through the odometry information, creating nodes for each point and the non-odometry nodes that connect to it, along with edges to connect for them.

The only odd portion of As Graph is the creation of dummy nodes. These were implemented as a solution to a problem involving bending of the graph. What would often happen was odometry orientation would be slightly off in successive measurements, causing the path to tilt up or down. When the G2O optimization was run on the graph, it would decide to change the orientation of the odometry nodes rather than the position to correct this tilt (think creating a U or V shape rather than moving points up or down to correct the tilt), resulting in a warped path rather than a straight one. This is because the error function only pays attention to te error between two adjacent nodes and the transformation between them, so warping the path appeared to the optimizer to be the best solution to minimize error, although this solution is obviously incorrect to the human eye.

Dummy nodes attempted to fix this by adding more nodes directly below the odometry nodes and giving them a very strong weight for orientation. This gave the optimizer incentive to leave the orientation the same and opt to move the position instead. Note that this solution was implemented before our more feasible weight sweeping methods, so it is possible that this issue is fixed by having better weights, though this is worth checking out (see To Do)

Graph Manager

GraphManager is the main class to handle anything to do with Graph, from optimization and evaluation to weight correlation. As such, there are many functions to cover.

Weight Evaluation and Comparison

As of 8/6/2021, there are three methods to evaluate a set of weights: ground truth, chi2, and Duncan's metric. Ground truth is based on physical measurements of tag locations relative to each other. Chi2 is the resulting chi2 from the graph optimization. While this is based on the weights, theoretically the weights should not matter so long as they are normalized properly. Duncan's metric is similar to chi2, thought it was originally created to try to subvert the issue with different magnitude weights. Below is a quick outline of the pros and cons of each:

Metric Pros Cons
Ground Truth
  • We know for certian that it is accurate and works
  • Painstaking to measure (unless we can find a way to use LiDAR meshes)
  • Need to remeasure for each map
  • Infeasible for anything bigger than a room
  • Does not take into accont odometry nodes
Chi2
  • Quick to calculate
  • Intuitive
  • Unsure if it is an accurate measure of weights
Duncan's Metric
  • While not especially quick to calculate, does not require measurement
  • Theoretically independent of weights used
  • Unsure if it is an accurate measure of weights
  • Preliminatry comparisons to ground truth are not promising for weights that are not normalized (at which pooint hopefully just chi2 will work?)

The methods to calculate these are:

Optimize Graph

Any other functions in GraphManager are just helpers and do not require rigorous explanation.

Scripts

While only one script is run on the sever, we have many that we use for testing purposes. Only a brief overview of what each does will be given here; to actually find how to run them, see the code itself and the README. If any command line arguments are given, they will be created in the corresponding file in a function called make_parser.

Process Graphs

process_graphs.py is the script that the server runs. The script itself is fairly basic: it first sets up a FirebaseManager and a GraphManager. It creates a function that will be called whenever Firebase updates, which will pull the map info, optimize the map, create a json, and upload it. Finally, it the FirebaseManager tells Firebase to run that function whenever it is updated.

Graph Manager User

graph_manager_user.py is essentially the manual version of process_graphs. It does, however, have a few more options, such as the ability to set the weights and the prescaling option (these are defaulted in process_graphs). It also has th eability to display a visualization of the optimized graphs(s).

Optimize Weights

optimize_weights.py is a script that attempts to find the best set of weights. It has two options to accomplish this. The first (and default) is a genetic machine learning model. As of 8/6/2021, it can tune al 12 parameters, but it is likely worth it to have it only tune 3 (as specified by graph_utils.weight_dict_from_array). The second method is a parameter sweep, where the script will try every combination of values in a certain range and determine the best one. If the sweep is 2-D, it will also display a visualization of the surface the resulting metric creates.

Correlate Metrics

This script was created to try to measure how good each metric was, especially when compared to ground truth. It visualizes the metrics and calculates the Spearman Correlation between each. The Spearman Correlation is essentially a measure of correlation that does not rely on linearity - i.e. any monotonically increasing function will have a Spearman Correlation of 1, even if it is nonlinear. This also has the option of instead loading the correlations from a saved file so as to not have to recalculate.

Visualize Chi2s

This is a script used when chi2s are suspiciously high, especially when trying to find weights. It will optimize the graphs with several different weight values, and display the resulting chi2 from each edge type.