code changes to increase modularity

release/4.3a0
akrishnan86 2020-09-12 13:31:45 -07:00
parent b81365e4bf
commit 43af7c4ae4
3 changed files with 175 additions and 134 deletions

View File

@ -7,123 +7,162 @@
#include <gtsam/sfm/MFAS.h>
#include <algorithm>
#include <map>
#include <unordered_map>
#include <vector>
using namespace gtsam;
using MFAS::KeyPair;
using std::map;
using std::pair;
using std::unordered_map;
using std::vector;
MFAS::MFAS(const std::shared_ptr<vector<Key>> &nodes,
// A node in the graph.
struct GraphNode {
double inWeightSum; // Sum of absolute weights of incoming edges.
double outWeightSum; // Sum of absolute weights of outgoing edges.
vector<Key> inNeighbors; // Nodes from which there is an incoming edge.
vector<Key> outNeighbors; // Nodes to which there is an outgoing edge.
// Heuristic for the node that is to select nodes in MFAS.
double heuristic() { return (outWeightSum + 1) / (inWeightSum + 1); }
};
// A graph is a map from key to GraphNode. This function returns the graph from
// the edgeWeights between keys.
unordered_map<Key, GraphNode> graphFromEdges(
const map<KeyPair, double>& edgeWeights) {
unordered_map<Key, GraphNode> graph;
for (const auto& [edge, weight] : edgeWeights) {
// The weights can be either negative or positive. The direction of the edge
// is the direction of positive weight. This means that the edges is from
// edge.first -> edge.second if weight is positive and edge.second ->
// edge.first if weight is negative.
Key edgeSource = weight >= 0 ? edge.first : edge.second;
Key edgeDest = weight >= 0 ? edge.second : edge.first;
// Update the in weight and neighbors for the destination.
graph[edgeDest].inWeightSum += std::abs(weight);
graph[edgeDest].inNeighbors.push_back(edgeSource);
// Update the out weight and neighbors for the source.
graph[edgeSource].outWeightSum += std::abs(weight);
graph[edgeSource].outNeighbors.push_back(edgeDest);
}
return graph;
}
// Selects the next node in the ordering from the graph.
Key selectNextNodeInOrdering(const unordered_map<Key, GraphNode>& graph) {
// Find the root nodes in the graph.
for (const auto& [key, node] : graph) {
// It is a root node if the inWeightSum is close to zero.
if (node.inWeightSum < 1e-8) {
// TODO(akshay-krishnan) if there are multiple roots, it is better to
// choose the one with highest heuristic. This is missing in the 1dsfm
// solution.
return key;
}
}
// If there are no root nodes, return the node with the highest heuristic.
return std::max_element(graph.begin(), graph.end(),
[](const std::pair<Key, GraphNode>& node1,
const std::pair<Key, GraphNode>& node2) {
return node1.second.heuristic() <
node2.second.heuristic();
})
->first;
}
// Returns the absolute weight of the edge between node1 and node2.
double absWeightOfEdge(const Key node1, const Key node2,
const map<KeyPair, double>& edgeWeights) {
// Check the direction of the edge before returning.
return edgeWeights_.find(KeyPair(node1, node2)) != edgeWeights_.end()
? std::abs(edgeWeights_.at(KeyPair(node1, node2)))
: std::abs(edgeWeights_.at(KeyPair(node2, node1)));
}
// Removes a node from the graph and updates edge weights of its neighbors.
void removeNodeFromGraph(const Key node, const map<KeyPair, double> edgeWeights,
unordered_map<Key, GraphNode>& graph) {
// Update the outweights and outNeighbors of node's inNeighbors
for (const Key neighbor : graph[node].inNeighbors) {
// the edge could be either (*it, choice) with a positive weight or
// (choice, *it) with a negative weight
graph[neighbor].outWeightSum -=
absWeightOfEdge(node, neighbor, edgeWeights);
graph[neighbor].outNeighbors.erase(graph[neighbor].outNeighbors.find(node));
}
// Update the inWeights and inNeighbors of node's outNeighbors
for (const Key neighbor : graph[node].outNeighbors) {
graph[neighbor].inWeightSum -= absWeightOfEdge(node, neighbor, edgeWeights);
graph[neighbor].inNeighbors.erase(graph[neighbor].inNeighbors.find(node));
}
// Erase node.
graph.erase(node);
}
MFAS::MFAS(const std::shared_ptr<vector<Key>>& nodes,
const TranslationEdges& relativeTranslations,
const Unit3 &projection_direction)
const Unit3& projectionDirection)
: nodes_(nodes) {
// iterate over edges, obtain weights by projecting
// Iterate over edges, obtain weights by projecting
// their relativeTranslations along the projection direction
for (auto it = relativeTranslations.begin();
it != relativeTranslations.end(); it++) {
edgeWeights_[it->first] = it->second.dot(projection_direction);
for (const auto& measurement : relativeTranslations) {
edgeWeights_[std::make_pair(measurement.key1(), measurement.key2())] =
measurement.measured().dot(projectionDirection);
}
}
std::vector<Key> MFAS::computeOrdering() const {
FastMap<Key, double> in_weights; // sum on weights of incoming edges for a node
FastMap<Key, double> out_weights; // sum on weights of outgoing edges for a node
FastMap<Key, vector<Key> > in_neighbors;
FastMap<Key, vector<Key> > out_neighbors;
vector<Key> MFAS::computeOrdering() const {
vector<Key> ordering; // Nodes in MFAS order (result).
vector<Key> ordered_nodes; // nodes in MFAS order (result)
FastMap<Key, int> ordered_positions; // map from node to its position in the output order
// A graph is an unordered map from keys to nodes. Each node contains a list
// of its adjacent nodes. Create the graph from the edgeWeights.
unordered_map<Key, GraphNode> graph = graphFromEdges(edgeWeights_);
// populate neighbors and weights
// Since the weights could be obtained by projection, they can be either
// negative or positive. Ideally, the weights should be positive in the
// direction of the edge. So, we define the direction of the edge as
// edge.first -> edge.second if weight is positive and
// edge.second -> edge.first if weight is negative. Once we know the
// direction, we only use the magnitude of the weights.
for (auto it = edgeWeights_.begin(); it != edgeWeights_.end(); it++) {
const KeyPair &edge = it->first;
const double weight = it->second;
Key edge_source = weight >= 0 ? edge.first : edge.second;
Key edge_dest = weight >= 0 ? edge.second : edge.first;
in_weights[edge_dest] += std::abs(weight);
out_weights[edge_source] += std::abs(weight);
in_neighbors[edge_dest].push_back(edge_source);
out_neighbors[edge_source].push_back(edge_dest);
// In each iteration, one node is removed from the graph and appended to the
// ordering.
while (!graph.empty()) {
Key selection = selectNextNodeInOrdering(graph);
removeNodeFromGraph(selection, edgeWeights_, graph);
ordering.push_back(selection);
}
// in each iteration, one node is appended to the ordered list
while (ordered_nodes.size() < nodes_->size()) {
// finding the node with the max heuristic score
Key choice;
double max_score = 0.0;
for (const Key &node : *nodes_) {
// if this node has not been chosen so far
if (ordered_positions.find(node) == ordered_positions.end()) {
// is this a root node
if (in_weights[node] < 1e-8) {
// TODO(akshay-krishnan) if there are multiple roots, it is better to choose the
// one with highest heuristic. This is missing in the 1dsfm solution.
choice = node;
break;
} else {
double score = (out_weights[node] + 1) / (in_weights[node] + 1);
if (score > max_score) {
max_score = score;
choice = node;
}
}
}
}
// find its in_neighbors, adjust their out_weights
for (auto it = in_neighbors[choice].begin();
it != in_neighbors[choice].end(); ++it)
// the edge could be either (*it, choice) with a positive weight or (choice, *it) with a negative weight
out_weights[*it] -= edgeWeights_.find(KeyPair(*it, choice)) == edgeWeights_.end() ? -edgeWeights_.at(KeyPair(choice, *it)) : edgeWeights_.at(KeyPair(*it, choice));
// find its out_neighbors, adjust their in_weights
for (auto it = out_neighbors[choice].begin();
it != out_neighbors[choice].end(); ++it)
in_weights[*it] -= edgeWeights_.find(KeyPair(choice, *it)) == edgeWeights_.end() ? -edgeWeights_.at(KeyPair(*it, choice)) : edgeWeights_.at(KeyPair(choice, *it));
ordered_positions[choice] = ordered_nodes.size();
ordered_nodes.push_back(choice);
}
return ordered_nodes;
return ordering;
}
std::map<MFAS::KeyPair, double> MFAS::computeOutlierWeights() const {
vector<Key> ordered_nodes = computeOrdering();
FastMap<Key, int> ordered_positions;
std::map<KeyPair, double> outlier_weights;
std::map<KeyPair, double> MFAS::computeOutlierWeights() const {
// Find the ordering.
vector<Key> ordering = computeOrdering();
// create a map becuase it is much faster to lookup the position of each node
// TODO(akshay-krishnan) this is already computed in computeOrdering. Would be nice if
// we could re-use. Either use an optional argument or change the output of
// computeOrdering
for(unsigned int i = 0; i < ordered_nodes.size(); i++) {
ordered_positions[ordered_nodes[i]] = i;
// Create a map from the node key to its position in the ordering. This makes
// it easier to lookup positions of different nodes.
unordered_map<Key, int> orderingPositions;
for (size_t i = 0; i < ordering.size(); i++) {
orderingPositions[ordering[i]] = i;
}
// iterate over all edges
for (auto it = edgeWeights_.begin(); it != edgeWeights_.end(); it++) {
Key edge_source, edge_dest;
if(it->second > 0) {
edge_source = it->first.first;
edge_dest = it->first.second;
} else {
edge_source = it->first.second;
edge_dest = it->first.first;
map<KeyPair, double> outlierWeights;
// Check if the direction of each edge is consistent with the ordering.
for (const auto& [edge, weight] : edgeWeights_) {
// Find edge source and destination.
Key source = edge.first;
Key dest = edge.second;
if (weight < 0) {
std::swap(source, dest);
}
// if the ordered position of nodes is not consistent with the edge
// direction for consistency second should be greater than first
if (ordered_positions.at(edge_dest) < ordered_positions.at(edge_source)) {
outlier_weights[it->first] = std::abs(edgeWeights_.at(it->first));
// If the direction is not consistent with the ordering (i.e dest occurs
// before src), it is an outlier edge, and has non-zero outlier weight.
if (orderingPositions.at(dest) < orderingPositions.at(source)) {
outlierWeights[edge] = std::abs(weight);
} else {
outlier_weights[it->first] = 0;
outlierWeights[edge] = 0;
}
}
return outlier_weights;
return outlierWeights;
}

View File

@ -15,14 +15,15 @@
* @file MFAS.h
* @brief MFAS class to solve Minimum Feedback Arc Set graph problem
* @author Akshay Krishnan
* @date July 2020
* @date September 2020
*/
#include <gtsam/geometry/Unit3.h>
#include <gtsam/inference/Key.h>
#include <gtsam/sfm/BinaryMeasurement.h>
#include <map>
#include <memory>
#include <unordered_map>
#include <vector>
namespace gtsam {
@ -30,29 +31,29 @@ namespace gtsam {
/**
The MFAS class to solve a Minimum feedback arc set (MFAS)
problem. We implement the solution from:
Kyle Wilson and Noah Snavely, "Robust Global Translations with 1DSfM",
Kyle Wilson and Noah Snavely, "Robust Global Translations with 1DSfM",
Proceedings of the European Conference on Computer Vision, ECCV 2014
Given a weighted directed graph, the objective in a Minimum feedback arc set
problem is to obtain a directed acyclic graph by removing
edges such that the total weight of removed edges is minimum.
Although MFAS is a general graph problem and can be applied in many areas, this
classed was designed for the purpose of outlier rejection in a
translation averaging for SfM setting. For more details, refer to the above paper.
The nodes of the graph in this context represents cameras in 3D and the edges
between them represent unit translations in the world coordinate frame, i.e
w_aZb is the unit translation from a to b expressed in the world coordinate frame.
The weights for the edges are obtained by projecting the unit translations in a
projection direction.
Although MFAS is a general graph problem and can be applied in many areas,
this classed was designed for the purpose of outlier rejection in a
translation averaging for SfM setting. For more details, refer to the above
paper. The nodes of the graph in this context represents cameras in 3D and the
edges between them represent unit translations in the world coordinate frame,
i.e w_aZb is the unit translation from a to b expressed in the world
coordinate frame. The weights for the edges are obtained by projecting the
unit translations in a projection direction.
@addtogroup SFM
*/
class MFAS {
public:
// used to represent edges between two nodes in the graph. When used in
// used to represent edges between two nodes in the graph. When used in
// translation averaging for global SfM
using KeyPair = std::pair<Key, Key>;
using TranslationEdges = std::map<KeyPair, Unit3>;
using TranslationEdges = std::vector<BinaryMeasurement<Unit3>>;
private:
// pointer to nodes in the graph
@ -65,29 +66,30 @@ class MFAS {
public:
/**
* @brief Construct from the nodes in a graph and weighted directed edges
* between the nodes. Each node is identified by a Key.
* between the nodes. Each node is identified by a Key.
* A shared pointer to the nodes is used as input parameter
* because, MFAS ordering is usually used to compute the ordering of a
* large graph that is already stored in memory. It is unnecessary make a
* copy of the nodes in this class.
* because, MFAS ordering is usually used to compute the ordering of a
* large graph that is already stored in memory. It is unnecessary to make a
* copy of the nodes in this class.
* @param nodes: Nodes in the graph
* @param edgeWeights: weights of edges in the graph
*/
MFAS(const std::shared_ptr<std::vector<Key>> &nodes,
const std::map<KeyPair, double> &edgeWeights) :
nodes_(nodes), edgeWeights_(edgeWeights) {}
const std::map<KeyPair, double> &edgeWeights)
: nodes_(nodes), edgeWeights_(edgeWeights) {}
/**
* @brief Constructor to be used in the context of translation averaging. Here,
* the nodes of the graph are cameras in 3D and the edges have a unit translation
* direction between them. The weights of the edges is computed by projecting
* them along a projection direction.
* @param nodes cameras in the epipolar graph (each camera is identified by a Key)
* @brief Constructor to be used in the context of translation averaging.
* Here, the nodes of the graph are cameras in 3D and the edges have a unit
* translation direction between them. The weights of the edges is computed by
* projecting them along a projection direction.
* @param nodes cameras in the epipolar graph (each camera is identified by a
* Key)
* @param relativeTranslations translation directions between the cameras
* @param projectionDirection direction in which edges are to be projected
*/
MFAS(const std::shared_ptr<std::vector<Key>> &nodes,
const TranslationEdges& relativeTranslations,
const TranslationEdges &relativeTranslations,
const Unit3 &projectionDirection);
/**
@ -97,10 +99,10 @@ class MFAS {
std::vector<Key> computeOrdering() const;
/**
* @brief Computes the "outlier weights" of the graph. We define the outlier weight
* of a edge to be zero if the edge is an inlier and the magnitude of its edgeWeight
* if it is an outlier. This function internally calls computeOrdering and uses the
* obtained ordering to identify outlier edges.
* @brief Computes the "outlier weights" of the graph. We define the outlier
* weight of a edge to be zero if the edge is an inlier and the magnitude of
* its edgeWeight if it is an outlier. This function internally calls
* computeOrdering and uses the obtained ordering to identify outlier edges.
* @return outlierWeights: map from an edge to its outlier weight.
*/
std::map<KeyPair, double> computeOutlierWeights() const;

View File

@ -22,7 +22,7 @@ using namespace gtsam;
*/
// edges in the graph - last edge from node 3 to 0 is an outlier
vector<MFAS::KeyPair> graph = {make_pair(3, 2), make_pair(0, 1), make_pair(3, 1),
vector<MFAS::KeyPair> edges = {make_pair(3, 2), make_pair(0, 1), make_pair(3, 1),
make_pair(1, 2), make_pair(0, 2), make_pair(3, 0)};
// nodes in the graph
vector<Key> nodes = {Key(0), Key(1), Key(2), Key(3)};
@ -33,11 +33,11 @@ vector<double> weights2 = {0.5, 0.75, -0.25, 0.75, 1, 0.5};
// helper function to obtain map from keypairs to weights from the
// vector representations
std::map<MFAS::KeyPair, double> getEdgeWeights(const vector<MFAS::KeyPair> &graph,
map<MFAS::KeyPair, double> getEdgeWeights(const vector<MFAS::KeyPair> &edges,
const vector<double> &weights) {
std::map<MFAS::KeyPair, double> edgeWeights;
for (size_t i = 0; i < graph.size(); i++) {
edgeWeights[graph[i]] = weights[i];
map<MFAS::KeyPair, double> edgeWeights;
for (size_t i = 0; i < edges.size(); i++) {
edgeWeights[edges[i]] = weights[i];
}
cout << "returning edge weights " << edgeWeights.size() << endl;
return edgeWeights;
@ -46,7 +46,7 @@ std::map<MFAS::KeyPair, double> getEdgeWeights(const vector<MFAS::KeyPair> &grap
// test the ordering and the outlierWeights function using weights2 - outlier
// edge is rejected when projected in a direction that gives weights2
TEST(MFAS, OrderingWeights2) {
MFAS mfas_obj(make_shared<vector<Key>>(nodes), getEdgeWeights(graph, weights2));
MFAS mfas_obj(make_shared<vector<Key>>(nodes), getEdgeWeights(edges, weights2));
vector<Key> ordered_nodes = mfas_obj.computeOrdering();
@ -62,7 +62,7 @@ TEST(MFAS, OrderingWeights2) {
// since edge between 3 and 0 is inconsistent with the ordering, it must have
// positive outlier weight, other outlier weights must be zero
for (auto &edge : graph) {
for (auto &edge : edges) {
if (edge == make_pair(Key(3), Key(0)) ||
edge == make_pair(Key(0), Key(3))) {
EXPECT_DOUBLES_EQUAL(outlier_weights[edge], 0.5, 1e-6);
@ -76,7 +76,7 @@ TEST(MFAS, OrderingWeights2) {
// weights1 (outlier edge is accepted when projected in a direction that
// produces weights1)
TEST(MFAS, OrderingWeights1) {
MFAS mfas_obj(make_shared<vector<Key>>(nodes), getEdgeWeights(graph, weights1));
MFAS mfas_obj(make_shared<vector<Key>>(nodes), getEdgeWeights(edges, weights1));
vector<Key> ordered_nodes = mfas_obj.computeOrdering();
@ -92,7 +92,7 @@ TEST(MFAS, OrderingWeights1) {
// since edge between 3 and 0 is inconsistent with the ordering, it must have
// positive outlier weight, other outlier weights must be zero
for (auto &edge : graph) {
for (auto &edge : edges) {
EXPECT_DOUBLES_EQUAL(outlier_weights[edge], 0, 1e-6);
}
}