Updated multisession_3it integration tests to compare loop closure hypotheses

This commit is contained in:
matlabbe
2026-08-20 21:49:34 -07:00
parent 6263b7e200
commit 0a38d5c869
3 changed files with 286 additions and 193 deletions
+52 -72
View File
@@ -289,9 +289,7 @@ protected:
// Constructor Tests // Constructor Tests
// Bayes/SparsePrediction changes how the prediction is held and multiplied, not what the // Bayes/SparsePrediction changes how the prediction is held and multiplied, not what the
// posterior is, so every test below that reads the posterior runs in both forms. The ones // posterior is, so every test that reads the posterior runs in both forms.
// that read the prediction matrix instead keep the dense form, which is the only one that
// has a matrix.
class BayesFilterModeTest : public ::testing::TestWithParam<bool> class BayesFilterModeTest : public ::testing::TestWithParam<bool>
{ {
protected: protected:
@@ -466,8 +464,7 @@ TEST(BayesFilterTest, ComputePosteriorEmptyLikelihood)
// generatePrediction Tests (virtual place only, no graph) // generatePrediction Tests (virtual place only, no graph)
// Nothing has been computed yet, so there is no sparse form to keep and the matrix is built // Nothing computed yet, so there is no sparse form and the matrix is built in both cases.
// in both cases.
TEST_P(BayesFilterModeTest, GeneratePredictionVirtualPlaceOnly) TEST_P(BayesFilterModeTest, GeneratePredictionVirtualPlaceOnly)
{ {
ParametersMap params = modeParams(); ParametersMap params = modeParams();
@@ -483,9 +480,8 @@ TEST_P(BayesFilterModeTest, GeneratePredictionVirtualPlaceOnly)
EXPECT_FLOAT_EQ(prediction.at<float>(0, 0), 1.0f); EXPECT_FLOAT_EQ(prediction.at<float>(0, 0), 1.0f);
} }
// The prediction matrix is kept between iterations and returned as it is while the ids do // The matrix is kept between iterations and returned as it is while the ids do not change.
// not change. Only the dense mode has a matrix at all: with the prediction kept sparse, // The sparse form has none to keep: generatePrediction() expands one per call.
// generatePrediction() has to build one to return, so there is nothing to keep.
TEST(BayesFilterTest, GeneratePredictionCachedWhenIdsUnchanged) TEST(BayesFilterTest, GeneratePredictionCachedWhenIdsUnchanged)
{ {
ParametersMap params; ParametersMap params;
@@ -563,9 +559,9 @@ TEST_P(BayesFilterModeTest, ComputePosteriorUpdatesWithNewLikelihood)
// Integration tests with real Memory graph // Integration tests with real Memory graph
// The columns of the prediction, read from the matrix. A map this small is never kept sparse, // The columns of the prediction, read from the matrix. A map this small is never kept sparse,
// a column of it reaching more than a quarter of the locations, so this is the dense form // a column reaching more than a quarter of the locations, so this is the dense form whatever
// whatever Bayes/SparsePrediction says; GeneratePredictionExpandsTheSparseFormIntoTheSameMatrix // Bayes/SparsePrediction says; GeneratePredictionExpandsTheSparseFormIntoTheSameMatrix carries
// carries these values over to the sparse form on a map large enough to keep it. // these values over to the sparse form on a map large enough to keep it.
TEST_F(BayesFilterMemoryFixture, GeneratePredictionLinearChain) TEST_F(BayesFilterMemoryFixture, GeneratePredictionLinearChain)
{ {
addChain(5); addChain(5);
@@ -635,9 +631,8 @@ TEST_F(BayesFilterMemoryFixture, GeneratePredictionLinearChain)
(float)predictionLC[1]}); (float)predictionLC[1]});
} }
// A model whose values sum to less than 1 has normalize() spread the difference over every // A model summing to less than 1 has normalize() spread the difference over every zero of a
// zero of a column, so there is nothing sparse to keep and the matrix is the form used with // column, so nothing is sparse to keep and the matrix is used either way.
// Bayes/SparsePrediction enabled as well as disabled. The columns below are its values.
TEST_P(BayesFilterMemoryModeFixture, GeneratePredictionNormalizesWhenSumBelowOne) TEST_P(BayesFilterMemoryModeFixture, GeneratePredictionNormalizesWhenSumBelowOne)
{ {
addChain(4); addChain(4);
@@ -696,9 +691,8 @@ TEST_P(BayesFilterMemoryModeFixture, GeneratePredictionNormalizesWhenSumBelowOne
(float)predictionLC[1] * scaleRatio}); (float)predictionLC[1] * scaleRatio});
} }
// The matrix is kept between iterations and handed over as it is while the ids do not change. // The matrix is handed over as it is while the ids do not change; the sparse form is expanded
// The sparse form is expanded into a matrix per call instead: it is the caller asking to read // per call, so the caller reading the prediction pays for it, not every iteration.
// the prediction that pays for it, not every iteration.
TEST_P(BayesFilterMemoryModeFixture, GeneratePredictionCachedWithGraph) TEST_P(BayesFilterMemoryModeFixture, GeneratePredictionCachedWithGraph)
{ {
// Large enough for the sparse form to be worth keeping, so that both forms are exercised. // Large enough for the sparse form to be worth keeping, so that both forms are exercised.
@@ -1097,9 +1091,7 @@ TEST_F(BayesFilterMemoryFixture, FullPredictionUpdateRegeneratesMatrix)
expectMatrixGrowsOnNewNode(false); expectMatrixGrowsOnNewNode(false);
} }
// Bayes/SparsePrediction only changes how the prediction matrix is multiplied with // Both forms must agree, including while the graph grows and the prediction is rebuilt.
// the last posterior, so both modes must agree, including while the graph grows and
// the matrix is rebuilt (the sparse view has to be rebuilt with it).
TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModes) TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModes)
{ {
ParametersMap paramsDense; ParametersMap paramsDense;
@@ -1140,9 +1132,8 @@ TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModes)
} }
} }
// A model whose values sum to less than 1 has normalize() spread the difference over // A model summing to less than 1 leaves the matrix dense, so the sparse mode falls back to
// every zero of a column, leaving the matrix dense. The sparse mode then falls back // the dense multiplication, which must not change the posterior.
// to the dense multiplication, which must not change the posterior.
TEST_F(BayesFilterMemoryFixture, SparsePredictionFallsBackWhenModelSumsBelowOne) TEST_F(BayesFilterMemoryFixture, SparsePredictionFallsBackWhenModelSumsBelowOne)
{ {
addChain(5); addChain(5);
@@ -1173,9 +1164,7 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionFallsBackWhenModelSumsBelowOne)
EXPECT_EQ(filterDense.getMemoryUsed(), filterSparse.getMemoryUsed()); EXPECT_EQ(filterDense.getMemoryUsed(), filterSparse.getMemoryUsed());
} }
// The sparse prediction holds only the non-zero values, so it is a fraction of what the // The sparse form holds only the non-zero values, so it comes out smaller than the matrix.
// matrix would be. Against the dense filter, which holds the matrix and no sparse form,
// this one holds the sparse form and no matrix, and comes out smaller.
TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed) TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed)
{ {
addChain(40); addChain(40);
@@ -1205,12 +1194,10 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionMemoryUsed)
EXPECT_LT(sparse, dense); EXPECT_LT(sparse, dense);
} }
// Where the sparse and dense multiplications have to agree exactly rather than within // Where the two multiplications have to agree exactly rather than within the rounding of
// the rounding of their sums: after an iteration whose likelihood is zero everywhere // their sums: a posterior of 1 on one location and 0 elsewhere makes the prior one column of
// but on one location, the posterior is 1 there and 0 elsewhere, so the next prior is // the prediction, each value a single product. The same floats then, which only holds if the
// one column of the prediction matrix, each of its values the result of a single // sparse form has the values of the matrix at the same rows and columns.
// product. Both must then return the very same floats, which is only true if the
// sparse view holds the values of the matrix at the same rows and columns.
TEST_F(BayesFilterMemoryFixture, SparsePredictionIsExactOnASingleColumn) TEST_F(BayesFilterMemoryFixture, SparsePredictionIsExactOnASingleColumn)
{ {
addChain(30); addChain(30);
@@ -1254,10 +1241,8 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionIsExactOnASingleColumn)
} }
} }
// In localization mode the prediction is built in its sparse form directly, the matrix // In localization mode the sparse form is built directly, the matrix never allocated. It is a
// never being allocated, which is a second implementation of the same probabilities: it // second implementation of the same probabilities, compared here over a fixed graph.
// has to give what the matrix gives. Compared over a fixed graph, on which the
// prediction is generated once and kept.
TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModesInLocalizationMode) TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModesInLocalizationMode)
{ {
addChain(30); addChain(30);
@@ -1295,11 +1280,9 @@ TEST_F(BayesFilterMemoryFixture, CompareSparsePredictionModesInLocalizationMode)
} }
} }
// The same exactness check as SparsePredictionIsExactOnASingleColumn, but against the // SparsePredictionIsExactOnASingleColumn against the prediction built directly in its sparse
// prediction built directly in its sparse form: a posterior that is 1 on one location // form: the same floats, which says that build puts the same probabilities at the same rows
// and 0 elsewhere makes each value of the prior a single product, so the two builds have // and columns as the matrix.
// to return the very same floats. This is what says that the sparse build puts the same
// probabilities at the same rows and columns as the matrix does.
TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeIsExactOnASingleColumn) TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeIsExactOnASingleColumn)
{ {
addChain(30); addChain(30);
@@ -1341,8 +1324,8 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeIsExactOnASin
} }
} }
// What the sparse build is for: the prediction matrix, which is the size of the working // What the sparse build is for: the matrix, the size of the working memory squared, is never
// memory squared, is not allocated at all. // allocated.
TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeDoesNotAllocateTheMatrix) TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeDoesNotAllocateTheMatrix)
{ {
addChain(200); addChain(200);
@@ -1368,11 +1351,10 @@ TEST_F(BayesFilterMemoryFixture, SparsePredictionInLocalizationModeDoesNotAlloca
EXPECT_LT(filterSparse.getMemoryUsed(), filterDense.getMemoryUsed()/4); EXPECT_LT(filterSparse.getMemoryUsed(), filterDense.getMemoryUsed()/4);
} }
// The sparse form is only kept while the prediction is sparse enough to be worth it, so one // One session can use both forms: a map too small for the sparse form to pay off starts on
// session can use both forms: a map too small for the sparse form to pay off starts on the // the matrix and grows into the sparse form. The matrix cannot be carried over across the
// matrix and grows into the sparse form. The matrix cannot be carried over across the // iterations the sparse form gave the prediction, the locations having moved on, so coming
// iterations the sparse form gave the prediction, the locations having moved on meanwhile, so // back to it has to build it again.
// coming back to it has to build it again.
TEST_F(BayesFilterMemoryFixture, PredictionCrossesFromSparseBackToTheMatrix) TEST_F(BayesFilterMemoryFixture, PredictionCrossesFromSparseBackToTheMatrix)
{ {
ParametersMap paramsDense; ParametersMap paramsDense;
@@ -1385,9 +1367,9 @@ TEST_F(BayesFilterMemoryFixture, PredictionCrossesFromSparseBackToTheMatrix)
BayesFilter filterDense(paramsDense); BayesFilter filterDense(paramsDense);
BayesFilter filterSparse(paramsSparse); BayesFilter filterSparse(paramsSparse);
// A column of this model reaches 3 locations on each side, which is more than a quarter of // A column of this model reaches 3 locations on each side: more than a quarter of a small
// a small map and less than a quarter of a larger one: the session starts on the matrix // map, less of a larger one, so the session starts on the matrix and grows into the
// and crosses over to the sparse form as it grows. // sparse form.
addChain(6); addChain(6);
std::vector<int> ids; std::vector<int> ids;
for(int iter = 0; iter < 40; ++iter) for(int iter = 0; iter < 40; ++iter)
@@ -1412,8 +1394,8 @@ TEST_F(BayesFilterMemoryFixture, PredictionCrossesFromSparseBackToTheMatrix)
// Grown into the sparse form, so the matrix of the first iterations is gone. // Grown into the sparse form, so the matrix of the first iterations is gone.
ASSERT_TRUE(filterSparse.isPredictionSparse()); ASSERT_TRUE(filterSparse.isPredictionSparse());
// Back to the matrix, which was last built 40 iterations and as many locations ago. It // Back to the matrix, last built 40 iterations and as many locations ago: built again
// has to be built again rather than updated against locations it was never built for. // rather than updated against locations it was never built for.
ParametersMap disableSparse; ParametersMap disableSparse;
disableSparse.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false")); disableSparse.insert(ParametersPair(Parameters::kBayesSparsePrediction(), "false"));
filterSparse.parseParameters(disableSparse); filterSparse.parseParameters(disableSparse);
@@ -1439,11 +1421,10 @@ TEST_F(BayesFilterMemoryFixture, PredictionCrossesFromSparseBackToTheMatrix)
} }
} }
// A location leaving the working memory, which memory management does on every iteration once // A location leaving the working memory, as memory management does on every iteration once the
// the map is larger than what it holds. The index of every location after it moves, so the // map outgrows it. The index of every location after it moves, so the sparse form is laid out
// sparse form is laid out again -- but the columns whose contents did not change are carried // again -- carrying over the columns whose contents did not change -- and what comes out has
// over rather than built again, and what comes out has to be the prediction the matrix holds, // to be the matrix value for value, with the same posterior.
// value for value, and give the same posterior.
TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenLocationsLeaveTheWorkingMemory) TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenLocationsLeaveTheWorkingMemory)
{ {
ParametersMap paramsDense; ParametersMap paramsDense;
@@ -1472,8 +1453,8 @@ TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenLocationsLeaveTheWorki
ASSERT_TRUE(filterSparse.isPredictionSparse()); ASSERT_TRUE(filterSparse.isPredictionSparse());
ASSERT_FALSE(filterDense.isPredictionSparse()); ASSERT_FALSE(filterDense.isPredictionSparse());
// Locations leaving, one iteration after the other, from the middle of the ones held as // Locations leaving from the middle of the ones held and from the oldest end: both move the
// well as from the oldest end: both move the index of the ones that stay. // index of the ones that stay.
std::vector<int> fewerIds = ids; std::vector<int> fewerIds = ids;
for(int iter = 0; iter < 5; ++iter) for(int iter = 0; iter < 5; ++iter)
{ {
@@ -1508,9 +1489,9 @@ TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenLocationsLeaveTheWorki
} }
} }
// A location coming back from long-term memory, which retrieval does when a hypothesis points // A location coming back from long-term memory, as retrieval does when a hypothesis points at
// at one that left. Its id is smaller than the ones added since, so it comes back in the // one that left. Its id being smaller than the ones added since, it comes back in the middle
// middle of the locations held rather than at the end, and the columns after it move. // of the locations held rather than at the end, and the columns after it move.
TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenALocationComesBack) TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenALocationComesBack)
{ {
ParametersMap paramsDense; ParametersMap paramsDense;
@@ -1568,16 +1549,16 @@ TEST_F(BayesFilterMemoryFixture, PredictionCarriesOverWhenALocationComesBack)
} }
// A graph linked densely enough that a column reaches more than a quarter of the map is not // A graph linked densely enough that a column reaches more than a quarter of the map is not
// kept sparse: a value costs 8 bytes against the 4 of a matrix cell, so past that point the // kept sparse: a value costs 8 bytes against the 4 of a matrix cell, so the matrix is cheaper
// matrix is the cheaper of the two and is what the filter builds. // past that point.
TEST_F(BayesFilterMemoryFixture, PredictionIsNotKeptSparseOnADenselyLinkedGraph) TEST_F(BayesFilterMemoryFixture, PredictionIsNotKeptSparseOnADenselyLinkedGraph)
{ {
addChain(40); addChain(40);
std::vector<int> ids = getBayesIds(); std::vector<int> ids = getBayesIds();
// Every location loop closed with the one a third of the map away, and a loop closure // Every location loop closed with the one a third of the map away. A loop closure costs no
// costs no depth in the graph search: the locations it joins share a column, and through // depth in the graph search, so the locations it joins share a column and through them a
// them a column comes to reach most of the map. // column comes to reach most of the map.
const cv::Mat infMatrix = cv::Mat::eye(6, 6, CV_64FC1); const cv::Mat infMatrix = cv::Mat::eye(6, 6, CV_64FC1);
const size_t third = ids.size()/3; const size_t third = ids.size()/3;
for(size_t i = 1; i < ids.size(); ++i) for(size_t i = 1; i < ids.size(); ++i)
@@ -1614,10 +1595,9 @@ TEST_F(BayesFilterMemoryFixture, PredictionIsNotKeptSparseOnADenselyLinkedGraph)
EXPECT_NEAR(sum, 1.0f, 1e-4f); EXPECT_NEAR(sum, 1.0f, 1e-4f);
} }
// generatePrediction() hands over the prediction whichever form it is held in, expanding the // generatePrediction() hands over the prediction whichever form holds it, expanding the sparse
// sparse form into a matrix for the caller. Rtabmap::dumpPrediction() reads it that way, and // one for the caller -- how Rtabmap::dumpPrediction() reads it. Both go through the same column
// so does anyone comparing the two forms: they are built through the same column arithmetic, // arithmetic, so the matrices have to come out equal value for value.
// so the matrices have to come out equal, value for value.
TEST_F(BayesFilterMemoryFixture, GeneratePredictionExpandsTheSparseFormIntoTheSameMatrix) TEST_F(BayesFilterMemoryFixture, GeneratePredictionExpandsTheSparseFormIntoTheSameMatrix)
{ {
// Large enough for the sparse form to be worth keeping, so that it is the one answering. // Large enough for the sparse form to be worth keeping, so that it is the one answering.
+233 -120
View File
@@ -46,6 +46,7 @@
#include "TestUtils.h" #include "TestUtils.h"
#include <opencv2/imgcodecs.hpp> #include <opencv2/imgcodecs.hpp>
#include <algorithm> #include <algorithm>
#include <cmath>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -112,8 +113,11 @@ struct ReplayResult
// Wall-clock seconds spent inside Odometry::process across all // Wall-clock seconds spent inside Odometry::process across all
// frames; divide by framesRead to get per-frame average. // frames; divide by framesRead to get per-frame average.
double odomTotalSeconds = 0.0; double odomTotalSeconds = 0.0;
// Timing/Posterior_computation (ms) over the frames that reported it, // The highest loop closure hypothesis of each node added, id and probability: the curve
// which is the prediction and the multiplication of the Bayes filter. // the Bayes filter draws over a session.
std::map<int, std::pair<int, float> > highestHypothesis;
// Timing/Posterior_computation (ms) over the frames that reported it: the prediction and
// the multiplication of the Bayes filter.
double posteriorMsSum = 0.0; double posteriorMsSum = 0.0;
float posteriorMsMin = -1.0f; float posteriorMsMin = -1.0f;
float posteriorMsMax = 0.0f; float posteriorMsMax = 0.0f;
@@ -562,13 +566,11 @@ ReplayResult replayDatabaseWithStoredOdom(
// the LaserScan of each SensorData after dbReader.takeData(), // the LaserScan of each SensorData after dbReader.takeData(),
// simulating a lidar with a tighter max range. // simulating a lidar with a tighter max range.
float scanMaxRange = 0.0f, float scanMaxRange = 0.0f,
// Starts a new map on every frame whose stored odom covariance is // Starts a new map on every frame whose stored odom covariance is the 9999 of a
// the 9999 of a session start, which is how rtabmap-reprocess // session start, as rtabmap-reprocess does. Off by default: a test wanting its own
// replays a database holding more than one session. Off by default: // boundaries uses the frame above.
// a test that wants its own boundaries uses the frame above.
bool triggerNewMapOnSessionStart = false, bool triggerNewMapOnSessionStart = false,
// Filled with the number of sessions the replay ran, meaning one // Filled with the number of sessions replayed: one plus the boundaries triggered on.
// plus the boundaries it triggered on.
int * sessionsReplayed = 0) int * sessionsReplayed = 0)
{ {
ReplayResult result; ReplayResult result;
@@ -675,11 +677,9 @@ ReplayResult replayDatabaseWithStoredOdom(
continue; continue;
} }
// Session boundary: the stored odom covariance of the first frame of // Session boundary: the 9999 covariance of a session's first frame says its pose does
// a session is the 9999 that says the pose does not continue the // not continue the previous one. Started before that frame is processed, so it is the
// previous one. The new map is started before that frame is // first of the new session rather than the last of the old. Same as rtabmap-reprocess.
// processed, so it is the first of the new session rather than the
// last of the one before. Same as rtabmap-reprocess.
if(triggerNewMapOnSessionStart if(triggerNewMapOnSessionStart
&& result.framesProcessed > 0 && result.framesProcessed > 0
&& info.odomCovariance.at<double>(0, 0) >= 9999.0) && info.odomCovariance.at<double>(0, 0) >= 9999.0)
@@ -740,6 +740,13 @@ ReplayResult replayDatabaseWithStoredOdom(
{ {
result.translationalRmseFinal = rmseIt->second; result.translationalRmseFinal = rmseIt->second;
} }
const auto hypIdIt = stats.data().find(Statistics::kLoopHighest_hypothesis_id());
const auto hypValIt = stats.data().find(Statistics::kLoopHighest_hypothesis_value());
if(hypIdIt != stats.data().end() && hypValIt != stats.data().end() && stats.refImageId()>0)
{
result.highestHypothesis[stats.refImageId()] =
std::make_pair((int)hypIdIt->second, hypValIt->second);
}
const auto postIt = stats.data().find(Statistics::kTimingPosterior_computation()); const auto postIt = stats.data().find(Statistics::kTimingPosterior_computation());
if(postIt != stats.data().end()) if(postIt != stats.data().end())
{ {
@@ -786,8 +793,7 @@ ReplayResult replayDatabaseWithStoredOdom(
return result; return result;
} }
// Where a node sits in the union-find of countConnectedComponents(), the path // Union-find root, halving the path on the way up.
// to it halved on the way up.
int graphComponentRoot(std::map<int, int> & parent, int id) int graphComponentRoot(std::map<int, int> & parent, int id)
{ {
while(parent.at(id) != id) while(parent.at(id) != id)
@@ -799,9 +805,8 @@ int graphComponentRoot(std::map<int, int> & parent, int id)
return id; return id;
} }
// How many pieces a graph is in: the nodes linked to each other, directly or // How many pieces a graph is in. Two sessions that never closed a loop with each other are
// through others, are one of them. Two sessions that never closed a loop with // two pieces.
// each other are two.
int countConnectedComponents( int countConnectedComponents(
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
const std::multimap<int, Link> & links) const std::multimap<int, Link> & links)
@@ -813,8 +818,7 @@ int countConnectedComponents(
} }
for(std::multimap<int, Link>::const_iterator iter=links.begin(); iter!=links.end(); ++iter) for(std::multimap<int, Link>::const_iterator iter=links.begin(); iter!=links.end(); ++iter)
{ {
// A link to a landmark, or to a node the graph does not hold, joins // A link to a landmark, or to a node the graph does not hold, joins nothing.
// nothing here.
if(parent.find(iter->second.from()) == parent.end() || if(parent.find(iter->second.from()) == parent.end() ||
parent.find(iter->second.to()) == parent.end()) parent.find(iter->second.to()) == parent.end())
{ {
@@ -835,37 +839,130 @@ int countConnectedComponents(
return (int)roots.size(); return (int)roots.size();
} }
// What replaying a database asks for on top of the parameters it was recorded with. // What replaying a database asks for on top of the parameters it was recorded with, kept as
// // little as possible: the dictionary and the searches stay as they were recorded, so what the
// The dictionary is the one the database holds, fixed: a dictionary built as the replay goes // replay does can be held against what the session did.
// feeds back on itself, the words a frame is quantized against depending on the ones the
// frames before it created, and a run then lands somewhere slightly different from the last.
// Handing it the vocabulary of the recorded session up front takes that out, and is also
// faster than building it again. The visual matching is exact for the same reason.
//
// Kp/NNStrategy is left as it is, approximate: the exact search is over the whole dictionary,
// 141k SIFT descriptors of 128 dimensions each, where a kd-tree is little better than
// comparing them all. Measured at over ten minutes against the twenty seconds of the
// approximate search, fixed dictionary or not.
ParametersMap replayParams(const ParametersMap & recordedWith, const std::string & srcPath) ParametersMap replayParams(const ParametersMap & recordedWith, const std::string & srcPath)
{ {
ParametersMap params = recordedWith; ParametersMap params = recordedWith;
// The nodes of the database are already the ones its detection rate kept, so every frame // Its nodes are already the ones its detection rate kept, so every frame is processed.
// the reader hands over is processed.
uInsert(params, ParametersPair(Parameters::kRtabmapDetectionRate(), "0")); uInsert(params, ParametersPair(Parameters::kRtabmapDetectionRate(), "0"));
// The features stored with each node rather than extracted again: this database keeps no // The features stored with each node: this database keeps no images to extract them from.
// images to extract them from.
uInsert(params, ParametersPair(Parameters::kMemUseOdomFeatures(), "true")); uInsert(params, ParametersPair(Parameters::kMemUseOdomFeatures(), "true"));
uInsert(params, ParametersPair(Parameters::kRGBDCreateOccupancyGrid(), "false")); uInsert(params, ParametersPair(Parameters::kRGBDCreateOccupancyGrid(), "false"));
uInsert(params, ParametersPair(Parameters::kKpIncrementalDictionary(), "false"));
uInsert(params, ParametersPair(Parameters::kKpDictionaryPath(), srcPath));
uInsert(params, ParametersPair(Parameters::kVisCorNNType(), "8")); // FLANN kd-tree single
return params; return params;
} }
// The optimized graph a database holds, which is what its own mapping session // The highest loop closure hypothesis the recorded session saw at each node, id and value,
// converged to and what a replay of it is compared against, together with the // from the statistics it saved. That is the curve a replay is compared against.
// parameters it was recorded with, which the replay is run with. std::map<int, std::pair<int, float> > loadDatabaseHighestHypothesis(const std::string & path)
{
std::map<int, std::pair<int, float> > hypothesis;
DBDriver * driver = DBDriver::create();
if(!driver->openConnection(path))
{
delete driver;
return hypothesis;
}
std::set<int> ids;
driver->getAllNodeIds(ids);
for(std::set<int>::const_iterator iter=ids.begin(); iter!=ids.end(); ++iter)
{
double stamp = 0.0;
const std::map<std::string, float> stats = driver->getStatistics(*iter, stamp);
const std::map<std::string, float>::const_iterator idIter =
stats.find(Statistics::kLoopHighest_hypothesis_id());
const std::map<std::string, float>::const_iterator valueIter =
stats.find(Statistics::kLoopHighest_hypothesis_value());
if(idIter != stats.end() && valueIter != stats.end())
{
hypothesis[*iter] = std::make_pair((int)idIter->second, valueIter->second);
}
}
driver->closeConnection(false);
delete driver;
return hypothesis;
}
// How a replay's highest hypothesis per node stands against the recorded one.
struct HypothesisComparison
{
int nodes = 0; ///< nodes compared, of the ones both curves have
int sameId = 0; ///< of those, the ones pointing at the very same location
int samePlace = 0; ///< and the ones pointing at a location within a meter of it
int alsoOverThreshold = 0; ///< and the ones the replay also took past the threshold
float meanAbsValue = 0.0f; ///< mean |value - golden value|
float maxAbsValue = 0.0f;
float sameIdRatio() const {return nodes>0 ? float(sameId)/float(nodes) : 0.0f;}
float samePlaceRatio() const {return nodes>0 ? float(samePlace)/float(nodes) : 0.0f;}
float overThresholdRatio() const {return nodes>0 ? float(alsoOverThreshold)/float(nodes) : 0.0f;}
};
// Two hypotheses are on the same place when the locations they point at are this close in the
// optimized graph. Ids cannot say that on a map of three passes over the same trajectory: the
// same corner is a node of each pass, hundreds of ids apart. The nodes are ~0.3 m apart along
// the path, so a meter is a handful of them, and the map is only 24 m by 33 m: a wider radius
// would call most of it the same place.
const float kHypothesisSamePlaceRadius = 1.0f; // meters
// Only the nodes where the recorded session had a hypothesis at or past Rtabmap/LoopThr are
// compared: under it the value is spread thinly over the working memory and which location
// comes out highest is noise.
HypothesisComparison compareHighestHypothesis(
const std::map<int, std::pair<int, float> > & golden,
const std::map<int, std::pair<int, float> > & replayed,
const std::map<int, Transform> & poses,
float loopThreshold)
{
HypothesisComparison c;
double sum = 0.0;
for(std::map<int, std::pair<int, float> >::const_iterator iter=golden.begin(); iter!=golden.end(); ++iter)
{
if(iter->second.first <= 0 || iter->second.second < loopThreshold)
{
continue;
}
const std::map<int, std::pair<int, float> >::const_iterator jter = replayed.find(iter->first);
if(jter == replayed.end())
{
continue;
}
++c.nodes;
if(jter->second.second >= loopThreshold)
{
++c.alsoOverThreshold;
}
const int goldenId = iter->second.first;
const int replayedId = jter->second.first;
if(goldenId == replayedId)
{
++c.sameId;
}
bool samePlace = goldenId == replayedId;
if(!samePlace && goldenId > 0 && replayedId > 0)
{
const std::map<int, Transform>::const_iterator goldenPose = poses.find(goldenId);
const std::map<int, Transform>::const_iterator replayedPose = poses.find(replayedId);
if(goldenPose != poses.end() && replayedPose != poses.end())
{
samePlace = goldenPose->second.getDistance(replayedPose->second)
< kHypothesisSamePlaceRadius;
}
}
if(samePlace)
{
++c.samePlace;
}
const float diff = fabs(iter->second.second - jter->second.second);
sum += diff;
c.maxAbsValue = std::max(c.maxAbsValue, diff);
}
c.meanAbsValue = c.nodes>0 ? (float)(sum/(double)c.nodes) : 0.0f;
return c;
}
// The optimized graph a database converged to, which a replay is compared against, and the
// parameters it was recorded with, which the replay runs with.
bool loadDatabaseGraphAndParameters( bool loadDatabaseGraphAndParameters(
const std::string & path, const std::string & path,
std::map<int, Transform> & optimizedPoses, std::map<int, Transform> & optimizedPoses,
@@ -2139,17 +2236,13 @@ TEST_F(RtabmapIntegrationFixture, Loop3ItGps)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Multi-session 2D lidar + SIFT (3 sessions, 935 nodes, ~270 m). // Multi-session 2D lidar + SIFT (3 sessions, 935 nodes, ~270 m).
// //
// The optimized graph the database already holds is the reference: it is what // The reference is the optimized graph the database holds: the same frames with the same
// the session that recorded it converged to, so replaying the same frames with // parameters have to find about as many loop closures. The replay reuses the features stored
// the same parameters has to land on the same trajectory and find about as many // with each node, the database keeping no images, and the ICP registration verifies loop
// loop closures. The database keeps no images, so the replay reuses the // closures on the scans it does keep.
// features stored with each node (Mem/UseOdomFeatures) rather than extracting
// any, and the scans it does keep are what the ICP registration verifies loop
// closures with.
// //
// Each run is done with the prediction of the Bayes filter held both as a // Run over both forms of the Bayes prediction, matrix and sparse: the same probabilities, so
// matrix and in its sparse form: the two are the same probabilities, so a real // a real session has to come out the same either way.
// session over a real graph has to come out the same either way.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_F(RtabmapIntegrationFixture, Multisession3It) TEST_F(RtabmapIntegrationFixture, Multisession3It)
{ {
@@ -2171,6 +2264,8 @@ TEST_F(RtabmapIntegrationFixture, Multisession3It)
ParametersMap params = replayParams(dbParams, srcPath); ParametersMap params = replayParams(dbParams, srcPath);
uInsert(params, ParametersPair(Parameters::kBayesSparsePrediction(), uInsert(params, ParametersPair(Parameters::kBayesSparsePrediction(),
sparsePrediction ? "true" : "false")); sparsePrediction ? "true" : "false"));
// The database carries the cap it was recorded with; this test is the run without it.
uInsert(params, ParametersPair(Parameters::kRtabmapMemoryThr(), "0"));
const std::string workDb = test::tempPath(uFormat( const std::string workDb = test::tempPath(uFormat(
"rtabmap_integration_Multisession3It_%s.db", label.c_str())); "rtabmap_integration_Multisession3It_%s.db", label.c_str()));
@@ -2206,40 +2301,24 @@ TEST_F(RtabmapIntegrationFixture, Multisession3It)
<< "ms min=" << result.posteriorMsMin << "ms min=" << result.posteriorMsMin
<< "ms max=" << result.posteriorMsMax << "ms\n"; << "ms max=" << result.posteriorMsMax << "ms\n";
// Every frame of the database is a node it kept, so the replay makes // Every frame is a node the database kept, so the replay makes as many, in 3 sessions.
// the same number of them, in the same three sessions.
EXPECT_EQ(sessions, 3) << label; EXPECT_EQ(sessions, 3) << label;
EXPECT_EQ(result.finalGlobalGraphSize, (int)goldenPoses.size()) << label; EXPECT_EQ(result.finalGlobalGraphSize, (int)goldenPoses.size()) << label;
// The same frames, the same parameters and the same features, so the // Loop closures are what this compares; the trajectory is printed but not asserted,
// trajectory comes out on top of the one the database holds: 7-15 cm // following from which closures a run happens to find. And a run is never the same
// and under 1.1 deg over six runs, dense and sparse alike. // twice: the registration accepts or rejects a hypothesis sitting on its threshold
// // from one run to the next, and one closure changes the next ones. Hence a band.
// The replay is still not the same twice, even over a fixed dictionary
// and an exact visual matching: the registration verifying a loop
// closure accepts or rejects a hypothesis sitting on its threshold
// from one run to the next, and one loop closure more or less pulls
// the graph. The bands are what that spread asks for, not what a
// single run would allow.
EXPECT_LT(tRmse, 0.3f) << label << " trajectory drifted from the reference";
EXPECT_LT(rRmse, 2.0f) << label << " orientation drifted from the reference";
// The reference graph holds 285 global loop closures; the replay finds
// 282-288 of them over six runs.
EXPECT_GT(result.loopClosuresAccepted, 265) << label << " found too few loop closures"; EXPECT_GT(result.loopClosuresAccepted, 265) << label << " found too few loop closures";
EXPECT_LT(result.loopClosuresAccepted, 305) << label << " found more loop closures than the reference"; EXPECT_LT(result.loopClosuresAccepted, 305) << label << " found more loop closures than the reference";
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The same replay under memory management: Rtabmap/MemoryThr caps the working // The same replay under memory management: Rtabmap/MemoryThr caps the working memory, so the
// memory, so the oldest nodes are transferred to long-term memory as the map // oldest nodes go to long-term memory as the map grows and only a window is ever held. The
// grows and only a window of it is ever held. What comes back is what the // global optimized graph still covers every node, and the loop closures found from a limited
// likelihood points at, the local retrieval being off, so this is the loop // working memory are fewer.
// closure hypotheses of the Bayes filter driving the whole window. The global
// optimized graph still covers every node, so it is compared against the same
// reference, and the loop closures found from a limited working memory are
// fewer.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_F(RtabmapIntegrationFixture, Multisession3ItMemoryThr) TEST_F(RtabmapIntegrationFixture, Multisession3ItMemoryThr)
{ {
@@ -2251,6 +2330,19 @@ TEST_F(RtabmapIntegrationFixture, Multisession3ItMemoryThr)
ASSERT_TRUE(loadDatabaseGraphAndParameters(srcPath, goldenPoses, dbParams)) ASSERT_TRUE(loadDatabaseGraphAndParameters(srcPath, goldenPoses, dbParams))
<< "No optimized graph in " << srcPath; << "No optimized graph in " << srcPath;
// The database was recorded with the same 300-node cap, so the highest hypothesis it saw
// at each node is the curve to compare against.
const std::map<int, std::pair<int, float> > goldenHypothesis =
loadDatabaseHighestHypothesis(srcPath);
ASSERT_GT(goldenHypothesis.size(), 900u) << "No saved statistics in " << srcPath;
// Under this a hypothesis is not worth comparing.
float loopThreshold = Parameters::defaultRtabmapLoopThr();
Parameters::parse(dbParams, Parameters::kRtabmapLoopThr(), loopThreshold);
ASSERT_GT(loopThreshold, 0.0f);
std::cerr << "[ ] Comparing hypotheses at or over " << Parameters::kRtabmapLoopThr()
<< "=" << loopThreshold << "\n";
struct Variant struct Variant
{ {
// "" leaves Bayes/SparsePrediction at its default, which is the sparse form. // "" leaves Bayes/SparsePrediction at its default, which is the sparse form.
@@ -2262,37 +2354,39 @@ TEST_F(RtabmapIntegrationFixture, Multisession3ItMemoryThr)
int maxLoops; int maxLoops;
int minLocalGraph; int minLocalGraph;
int maxLocalGraph; int maxLocalGraph;
float maxTransRmse; // meters // Of the nodes the recorded session had a hypothesis on, the least that must point at
float maxRotRmse; // degrees // the same place, and the most the probability may differ by on average.
float minSamePlaceRatio;
float maxMeanValueDiff;
std::string label; std::string label;
}; };
// The first two are the same run over both forms of the prediction, with what comes back // The first two are the same run over both forms of the prediction, sharing their band:
// from long-term memory driven by the likelihood alone, and they share their expectations: // which form holds it must not change what the session does. The last three hold the form
// the form the prediction is held in must not change what the session does. The last three // at its default and take the retrieval apart: none, local only, and both, which is what a
// hold the form at its default and take the retrieval apart: nothing coming back at all, // session runs with out of the box.
// the local retrieval on its own, and both of them together, which is what a session runs
// with out of the box.
// //
// The bands come from six runs of each, which is what it takes to see the spread: the // The bands come from six runs of each. Observed:
// registration verifying a loop closure accepts or rejects a hypothesis sitting on its
// threshold from one run to the next, and one loop closure more or less pulls the graph.
// Observed:
// //
// variant loops working memory trans rmse rot rmse // variant loops working same place also over value diff
// sparse 253-267 275-289 0.16-0.48 m 1.1-2.3 deg // memory as recorded threshold mean
// dense 247-273 271-282 0.13-0.48 m 0.9-2.4 deg // sparse 268-288 242-263 80-85% 86-91% 0.051-0.075
// no-retrieval 197-205 277-280 0.39-0.53 m 2.2-2.7 deg // dense 267-284 253-266 81-84% 86-91% 0.052-0.084
// local-retrieval-only 227-236 287-292 0.40-0.51 m 2.1-2.5 deg // no-retrieval 204-213 229-256 68-71% 81-86% 0.103-0.119
// both-retrieval 245-269 278-289 0.12-0.39 m 1.1-2.0 deg // local-retrieval-only 240-259 262-267 76-81% 85-91% 0.071-0.081
// both-retrieval 265-287 261-268 82-84% 89-92% 0.031-0.053
//
// The closest to the recorded session is the one configured like it, both retrievals on,
// and the furthest is the one with none: what comes back into the working memory is what
// the hypotheses are drawn over.
// //
// The posterior time each run prints is left unasserted: it is what these variants are // The posterior time each run prints is left unasserted: it is what these variants are
// measured for, but it is also what a loaded runner moves most. // measured for, but also what a loaded runner moves most.
const std::vector<Variant> variants = { const std::vector<Variant> variants = {
{"true", "0", "", 230, 290, 255, 310, 0.8f, 3.5f, "sparse" }, {"true", "0", "", 245, 310, 225, 290, 0.72f, 0.11f, "sparse" },
{"false", "0", "", 230, 290, 255, 310, 0.8f, 3.5f, "dense" }, {"false", "0", "", 245, 310, 225, 290, 0.72f, 0.11f, "dense" },
{"", "0", "0", 175, 225, 255, 305, 0.8f, 3.5f, "no-retrieval" }, {"", "0", "0", 180, 240, 210, 280, 0.60f, 0.16f, "no-retrieval" },
{"", "2", "0", 205, 260, 265, 320, 0.8f, 3.5f, "local-retrieval-only"}, {"", "2", "0", 215, 285, 240, 290, 0.68f, 0.11f, "local-retrieval-only"},
{"", "2", "2", 215, 300, 255, 315, 0.8f, 3.5f, "both-retrieval" }, {"", "2", "2", 240, 310, 240, 295, 0.74f, 0.08f, "both-retrieval" },
}; };
// Loop closures per variant, for the ordering between them. // Loop closures per variant, for the ordering between them.
@@ -2356,40 +2450,59 @@ TEST_F(RtabmapIntegrationFixture, Multisession3ItMemoryThr)
<< "ms min=" << result.posteriorMsMin << "ms min=" << result.posteriorMsMin
<< "ms max=" << result.posteriorMsMax << "ms\n"; << "ms max=" << result.posteriorMsMax << "ms\n";
// Nothing is lost whatever comes back: the nodes leave the working memory for // The highest hypothesis of each node against the one the recorded session saw:
// long-term memory, and the global graph still covers every one of them, in one // the same locations pointed at, with the same probability on them.
// piece. The three sessions of this database are three passes over the same const HypothesisComparison hyp = compareHighestHypothesis(
// trajectory, and 300 nodes of working memory is wide enough to still hold the end goldenHypothesis, result.highestHypothesis, goldenPoses, loopThreshold);
// of one session while the next starts over the same place, so the link across the std::cerr << "[" << v.label << "] hypothesis over " << hyp.nodes
// boundary is found even when nothing comes back at all. << " nodes the recorded session had one on: same id " << hyp.sameId
<< " (" << 100.0f*hyp.sameIdRatio() << "%), same place " << hyp.samePlace
<< " (" << 100.0f*hyp.samePlaceRatio() << "%), also over the threshold "
<< hyp.alsoOverThreshold << " (" << 100.0f*hyp.overThresholdRatio()
<< "%), value diff mean=" << hyp.meanAbsValue
<< " max=" << hyp.maxAbsValue << "\n";
// Nothing is lost: the nodes go to long-term memory and the global graph still covers
// every one, in one piece. The three sessions are three passes over the same
// trajectory and 300 nodes is wide enough to still hold the end of one when the next
// starts over the same place, so the link across the boundary is found even with
// nothing coming back.
EXPECT_EQ(sessions, 3) << v.label; EXPECT_EQ(sessions, 3) << v.label;
EXPECT_EQ(result.finalGlobalGraphSize, (int)goldenPoses.size()) << v.label; EXPECT_EQ(result.finalGlobalGraphSize, (int)goldenPoses.size()) << v.label;
EXPECT_EQ(components, 1) << v.label << " graph came out in pieces"; EXPECT_EQ(components, 1) << v.label << " graph came out in pieces";
// The working memory is the part that is capped, and it stays there, whatever comes // The capped part stays capped. Anything near 935 would mean the cap never took effect
// back into it. Anything near 935 would mean the cap never took effect and the test // and the test is no longer about memory management.
// is no longer about memory management.
EXPECT_GT(result.finalLocalGraphSize, v.minLocalGraph) << v.label; EXPECT_GT(result.finalLocalGraphSize, v.minLocalGraph) << v.label;
EXPECT_LT(result.finalLocalGraphSize, v.maxLocalGraph) EXPECT_LT(result.finalLocalGraphSize, v.maxLocalGraph)
<< v.label << " working memory was not capped"; << v.label << " working memory was not capped";
// Loop closures are only found against what the working memory holds, so what comes // Loop closures are only found against what the working memory holds.
// back into it is what they are found on.
EXPECT_GT(result.loopClosuresAccepted, v.minLoops) << v.label << " found too few loop closures"; EXPECT_GT(result.loopClosuresAccepted, v.minLoops) << v.label << " found too few loop closures";
EXPECT_LT(result.loopClosuresAccepted, v.maxLoops) << v.label << " found more loop closures than expected"; EXPECT_LT(result.loopClosuresAccepted, v.maxLoops) << v.label << " found more loop closures than expected";
// And the trajectory holds up on the strength of those. // The trajectory is printed but not asserted, following from which closures a run
EXPECT_LT(tRmse, v.maxTransRmse) << v.label << " trajectory drifted from the reference"; // happens to find.
EXPECT_LT(rRmse, v.maxRotRmse) << v.label << " orientation drifted from the reference";
// The recorded session had a hypothesis worth the name on 562 of its 935 nodes; the
// replay is held against those. Not the same one every time -- the sessions part ways
// as soon as they accept a different closure, and what a capped working memory holds
// follows from that -- but the same place on three quarters or more, and past the
// threshold on nine tenths.
EXPECT_GT(hyp.nodes, 400) << v.label << " compared too few nodes";
EXPECT_GT(hyp.samePlaceRatio(), v.minSamePlaceRatio)
<< v.label << " points at other places than the recorded session";
EXPECT_LT(hyp.meanAbsValue, v.maxMeanValueDiff)
<< v.label << " hypothesis probabilities drifted from the recorded session";
EXPECT_GT(hyp.overThresholdRatio(), 0.70f)
<< v.label << " left the recorded session's hypotheses under the threshold";
loopsPerVariant[v.label] = result.loopClosuresAccepted; loopsPerVariant[v.label] = result.loopClosuresAccepted;
} }
// What comes back is what the loop closures are found on, so the settings of it stand in // Whatever a run does inside its band, the retrieval settings stand in this order: none
// this order, whatever a run does inside its band: nothing coming back finds the fewest, // finds the fewest, local only finds more, and bringing back what the likelihood points at
// the local retrieval around the current pose finds more, and bringing back what the // finds the most.
// likelihood points at -- on its own or together with the local retrieval -- finds the
// most.
ASSERT_EQ(loopsPerVariant.size(), 5u); ASSERT_EQ(loopsPerVariant.size(), 5u);
EXPECT_GT(loopsPerVariant.at("local-retrieval-only"), loopsPerVariant.at("no-retrieval")); EXPECT_GT(loopsPerVariant.at("local-retrieval-only"), loopsPerVariant.at("no-retrieval"));
EXPECT_GT(loopsPerVariant.at("both-retrieval"), loopsPerVariant.at("no-retrieval")); EXPECT_GT(loopsPerVariant.at("both-retrieval"), loopsPerVariant.at("no-retrieval"));
+1 -1
View File
@@ -25,7 +25,7 @@ pr2_scan2d_corridor_50s.db 18ClEPTRM98icorrlDYBzKTWRwVOU8-bh f518cb4d9284b24a150
robust_graph_optimization_stereo.db https://raw.githubusercontent.com/wiki/introlab/rtabmap/doc/Tutorials/RobustGraphOptimization/robust_graph_optimization_stereo.db 247694b5bdb82168ebe88bb6bb5bc31c7142234f8c64567afd93e464418cf02e robust_graph_optimization_stereo.db https://raw.githubusercontent.com/wiki/introlab/rtabmap/doc/Tutorials/RobustGraphOptimization/robust_graph_optimization_stereo.db 247694b5bdb82168ebe88bb6bb5bc31c7142234f8c64567afd93e464418cf02e
loop_3it_gps.db https://raw.githubusercontent.com/wiki/introlab/rtabmap/doc/Tutorials/RobustGraphOptimization/loop_3it_gps.db 7aefeb573107a27b0a7826cb87ea03db495367aa56868b4d2b2543e6cacad3c8 loop_3it_gps.db https://raw.githubusercontent.com/wiki/introlab/rtabmap/doc/Tutorials/RobustGraphOptimization/loop_3it_gps.db 7aefeb573107a27b0a7826cb87ea03db495367aa56868b4d2b2543e6cacad3c8
stereo_20Hz.db https://github.com/introlab/rtabmap/releases/download/0.23.1/stereo_20Hz.db 94e219e1c96e540cbb1490cc23c81c65b9e7b132e8d61eda05e7990bc13393b7 stereo_20Hz.db https://github.com/introlab/rtabmap/releases/download/0.23.1/stereo_20Hz.db 94e219e1c96e540cbb1490cc23c81c65b9e7b132e8d61eda05e7990bc13393b7
multisession_3it.7z 10ulRDMqQy5V_3_kx_IeNzAJ8iUMdkkBZ 7b0b02108f05516870eea42cbb519aa6c46a0ffd1eb3e525a9920d3b1997b6f4 multisession_3it.db multisession_3it.7z 10ulRDMqQy5V_3_kx_IeNzAJ8iUMdkkBZ 3d7b139e6b4c608185e44774288ce078b6364bfaeecd0c46c51007c95230c987 multisession_3it.db
superpoint_v1.pth https://github.com/magicleap/SuperPointPretrainedNetwork/raw/refs/heads/master/superpoint_v1.pth 52b6708629640ca883673b5d5c097c4ddad37d8048b33f09c8ca0d69db12c40e superpoint_v1.pth https://github.com/magicleap/SuperPointPretrainedNetwork/raw/refs/heads/master/superpoint_v1.pth 52b6708629640ca883673b5d5c097c4ddad37d8048b33f09c8ca0d69db12c40e
superpoint_v6_from_tf.pth https://github.com/rpautrat/SuperPoint/raw/refs/heads/master/weights/superpoint_v6_from_tf.pth cd5d19a5061848e248c17728878ea166b66512076d43c77dbcf27f4a88a56084 superpoint_v6_from_tf.pth https://github.com/rpautrat/SuperPoint/raw/refs/heads/master/weights/superpoint_v6_from_tf.pth cd5d19a5061848e248c17728878ea166b66512076d43c77dbcf27f4a88a56084
demo_superpoint.py https://raw.githubusercontent.com/magicleap/SuperPointPretrainedNetwork/master/demo_superpoint.py 613706ae7e9ce3fbc2cfe042fc3f37d739838cc2e1dc8ddd08f8ec037765df04 demo_superpoint.py https://raw.githubusercontent.com/magicleap/SuperPointPretrainedNetwork/master/demo_superpoint.py 613706ae7e9ce3fbc2cfe042fc3f37d739838cc2e1dc8ddd08f8ec037765df04