mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-10 20:09:51 +08:00
Updated multisession_3it integration tests to compare loop closure hypotheses
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user