mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-04 09:07:47 +08:00
making some tests less flaky
This commit is contained in:
@@ -74,6 +74,34 @@ namespace util3d
|
|||||||
*
|
*
|
||||||
* @see cv::solvePnPRansac
|
* @see cv::solvePnPRansac
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @brief Toggle a deterministic seed for OpenGV's internal RANSAC RNG.
|
||||||
|
*
|
||||||
|
* OpenGV's @c SampleConsensusProblem (and its multi-camera sibling) seeds its
|
||||||
|
* internal @c std::mt19937 from the system clock when default-constructed,
|
||||||
|
* which makes every @ref estimateMotion3DTo2D() call non-reproducible across
|
||||||
|
* runs. Calling @c setRansacDeterministicSeed(true) reseeds OpenGV's RNG with
|
||||||
|
* the fixed value @c 12345 before each RANSAC pass so identical inputs always
|
||||||
|
* produce identical inlier sets, covariances and output transforms.
|
||||||
|
*
|
||||||
|
* Intended for tests; production code should leave this off (default).
|
||||||
|
*
|
||||||
|
* @param enable If true, force the deterministic seed; if false (default),
|
||||||
|
* use OpenGV's system-clock seed.
|
||||||
|
*
|
||||||
|
* @todo Expose this through the @c Parameters layer (e.g.
|
||||||
|
* @c kVisDeterministicRansacSeed) so production runs that need
|
||||||
|
* bit-for-bit replayability - reproducing a reported failure on the
|
||||||
|
* exact same input, or doing regression diffs across rtabmap versions -
|
||||||
|
* can opt in without code edits. Production should default to the
|
||||||
|
* wall-clock seed (occasional sample diversity still helps marginal
|
||||||
|
* inputs).
|
||||||
|
*/
|
||||||
|
void RTABMAP_CORE_EXPORT setRansacDeterministicSeed(bool enable);
|
||||||
|
|
||||||
|
/** @return Whether the deterministic-seed toggle is currently enabled. */
|
||||||
|
bool RTABMAP_CORE_EXPORT ransacDeterministicSeedEnabled();
|
||||||
|
|
||||||
Transform RTABMAP_CORE_EXPORT estimateMotion3DTo2D(
|
Transform RTABMAP_CORE_EXPORT estimateMotion3DTo2D(
|
||||||
const std::map<int, cv::Point3f> & words3A,
|
const std::map<int, cv::Point3f> & words3A,
|
||||||
const std::map<int, cv::KeyPoint> & words2B,
|
const std::map<int, cv::KeyPoint> & words2B,
|
||||||
|
|||||||
@@ -56,6 +56,24 @@ namespace rtabmap
|
|||||||
namespace util3d
|
namespace util3d
|
||||||
{
|
{
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// When true, every newly-constructed OpenGV SAC problem inside this
|
||||||
|
// translation unit gets its RNG reseeded with a fixed constant so that
|
||||||
|
// RANSAC is bit-for-bit reproducible. Tests can flip this on via
|
||||||
|
// setRansacDeterministicSeed(true); production code leaves it off.
|
||||||
|
bool g_ransacDeterministicSeed = false;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void setRansacDeterministicSeed(bool enable)
|
||||||
|
{
|
||||||
|
g_ransacDeterministicSeed = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ransacDeterministicSeedEnabled()
|
||||||
|
{
|
||||||
|
return g_ransacDeterministicSeed;
|
||||||
|
}
|
||||||
|
|
||||||
Transform estimateMotion3DTo2D(
|
Transform estimateMotion3DTo2D(
|
||||||
const std::map<int, cv::Point3f> & words3A,
|
const std::map<int, cv::Point3f> & words3A,
|
||||||
const std::map<int, cv::KeyPoint> & words2B,
|
const std::map<int, cv::KeyPoint> & words2B,
|
||||||
@@ -517,6 +535,18 @@ Transform estimateMotion3DTo2D(
|
|||||||
std::shared_ptr<opengv::sac_problems::absolute_pose::MultiNoncentralAbsolutePoseSacProblem> absposeproblem_ptr(
|
std::shared_ptr<opengv::sac_problems::absolute_pose::MultiNoncentralAbsolutePoseSacProblem> absposeproblem_ptr(
|
||||||
new opengv::sac_problems::absolute_pose::MultiNoncentralAbsolutePoseSacProblem(adapter));
|
new opengv::sac_problems::absolute_pose::MultiNoncentralAbsolutePoseSacProblem(adapter));
|
||||||
|
|
||||||
|
// Opt-in deterministic RANSAC: OpenGV's default constructor seeds
|
||||||
|
// its internal mt19937 from the system clock, so without this
|
||||||
|
// override two calls with identical inputs can produce different
|
||||||
|
// inlier sets / covariances. Tests flip the toggle via
|
||||||
|
// setRansacDeterministicSeed(true).
|
||||||
|
if(g_ransacDeterministicSeed)
|
||||||
|
{
|
||||||
|
absposeproblem_ptr->rng_alg_.seed(12345u);
|
||||||
|
absposeproblem_ptr->rng_gen_.reset(new std::function<int()>(
|
||||||
|
std::bind(*absposeproblem_ptr->rng_dist_, absposeproblem_ptr->rng_alg_)));
|
||||||
|
}
|
||||||
|
|
||||||
ransac.sac_model_ = absposeproblem_ptr;
|
ransac.sac_model_ = absposeproblem_ptr;
|
||||||
ransac.threshold_ = 1.0 - cos(atan(reprojError/cameraModels[0].fx()));
|
ransac.threshold_ = 1.0 - cos(atan(reprojError/cameraModels[0].fx()));
|
||||||
ransac.max_iterations_ = iterations;
|
ransac.max_iterations_ = iterations;
|
||||||
@@ -572,6 +602,14 @@ Transform estimateMotion3DTo2D(
|
|||||||
std::shared_ptr<opengv::sac_problems::absolute_pose::AbsolutePoseSacProblem> absposeproblem_ptr(
|
std::shared_ptr<opengv::sac_problems::absolute_pose::AbsolutePoseSacProblem> absposeproblem_ptr(
|
||||||
new opengv::sac_problems::absolute_pose::AbsolutePoseSacProblem(adapter, opengv::sac_problems::absolute_pose::AbsolutePoseSacProblem::GP3P));
|
new opengv::sac_problems::absolute_pose::AbsolutePoseSacProblem(adapter, opengv::sac_problems::absolute_pose::AbsolutePoseSacProblem::GP3P));
|
||||||
|
|
||||||
|
// Opt-in deterministic RANSAC (see comment on MultiRansac above).
|
||||||
|
if(g_ransacDeterministicSeed)
|
||||||
|
{
|
||||||
|
absposeproblem_ptr->rng_alg_.seed(12345u);
|
||||||
|
absposeproblem_ptr->rng_gen_.reset(new std::function<int()>(
|
||||||
|
std::bind(*absposeproblem_ptr->rng_dist_, absposeproblem_ptr->rng_alg_)));
|
||||||
|
}
|
||||||
|
|
||||||
ransac.sac_model_ = absposeproblem_ptr;
|
ransac.sac_model_ = absposeproblem_ptr;
|
||||||
ransac.threshold_ = 1.0 - cos(atan(reprojError/cameraModels[0].fx()));
|
ransac.threshold_ = 1.0 - cos(atan(reprojError/cameraModels[0].fx()));
|
||||||
ransac.max_iterations_ = iterations;
|
ransac.max_iterations_ = iterations;
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ static ParametersMap registrationVisTestParams(int estimationType = 1, int corTy
|
|||||||
params[Parameters::kVisCorType()] = std::to_string(corType); // 0=feature matching, 1=optical flow
|
params[Parameters::kVisCorType()] = std::to_string(corType); // 0=feature matching, 1=optical flow
|
||||||
params[Parameters::kVisBundleAdjustment()] = "0";
|
params[Parameters::kVisBundleAdjustment()] = "0";
|
||||||
params[Parameters::kVisRoiRatios()] = kRoiRatios;
|
params[Parameters::kVisRoiRatios()] = kRoiRatios;
|
||||||
|
if(corType == 1)
|
||||||
|
{
|
||||||
|
// Lucas-Kanade tracker defaults (win=16, levels=3) leave enough
|
||||||
|
// sub-pixel drift to push most correspondences past the 2 px
|
||||||
|
// reprojection bound on some OpenCV builds (SIMD/IPP differences in
|
||||||
|
// cv::calcOpticalFlowPyrLK). A larger window + more pyramid levels
|
||||||
|
// converge tighter and keep the OF test robust across platforms
|
||||||
|
// (~90% inlier ratio instead of ~5% on Ubuntu's DFSG OpenCV).
|
||||||
|
params[Parameters::kVisCorFlowWinSize()] = "21";
|
||||||
|
params[Parameters::kVisCorFlowMaxLevel()] = "5";
|
||||||
|
}
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,15 +145,6 @@ static Transform computeRegistration(
|
|||||||
return reg.computeTransformation(from, to, nullGuess, infoOut ? infoOut : &info);
|
return reg.computeTransformation(from, to, nullGuess, infoOut ? infoOut : &info);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Transform computeRegistration(
|
|
||||||
const SensorData & fromData,
|
|
||||||
const SensorData & toData,
|
|
||||||
int estimationType,
|
|
||||||
RegistrationInfo * infoOut = nullptr)
|
|
||||||
{
|
|
||||||
return computeRegistration(fromData, toData, registrationVisTestParams(estimationType), infoOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Golden transforms (GFTT/ORB, MinDistance=3, QualityLevel=0.01, MaxFeatures=3000, RoiRatios=0 0 0 0.3).
|
// Golden transforms (GFTT/ORB, MinDistance=3, QualityLevel=0.01, MaxFeatures=3000, RoiRatios=0 0 0 0.3).
|
||||||
// Captured with Vis/CorType=0 (feature matching); also used for optical flow (CorType=1) within tolerance.
|
// Captured with Vis/CorType=0 (feature matching); also used for optical flow (CorType=1) within tolerance.
|
||||||
// Shared by FM/OF, Vis/BundleAdjustment=0 and g2o BA=1.
|
// Shared by FM/OF, Vis/BundleAdjustment=0 and g2o BA=1.
|
||||||
|
|||||||
@@ -1017,6 +1017,14 @@ TEST_F(RtabmapFixture, RejectLastLoopClosureRemovesLinkAndResetsHypothesis)
|
|||||||
// so mapCorrection collapses back to identity.
|
// so mapCorrection collapses back to identity.
|
||||||
ParametersMap params = defaultRtabmapParams();
|
ParametersMap params = defaultRtabmapParams();
|
||||||
params[Parameters::kRGBDOptimizeMaxError()] = "0";
|
params[Parameters::kRGBDOptimizeMaxError()] = "0";
|
||||||
|
// TORO's default convergence epsilon (1e-5) stops the gradient descent
|
||||||
|
// while ~18 mm of correction still hasn't unwound. Tighten it so all
|
||||||
|
// three backends converge close enough to identity to satisfy the
|
||||||
|
// post-check below. Iterations stay at the default (100): bumping them
|
||||||
|
// further would let TORO reach ~1 mm, but the test's 1 cm bound is
|
||||||
|
// already comfortably above the ~6.5 mm TORO hits at 100 iterations
|
||||||
|
// with this epsilon, so the cheaper iteration budget is enough.
|
||||||
|
params[Parameters::kOptimizerEpsilon()] = "1e-10";
|
||||||
reinit(params);
|
reinit(params);
|
||||||
process();
|
process();
|
||||||
const int N1 = rtabmap_->getLastLocationId();
|
const int N1 = rtabmap_->getLastLocationId();
|
||||||
@@ -1039,8 +1047,14 @@ TEST_F(RtabmapFixture, RejectLastLoopClosureRemovesLinkAndResetsHypothesis)
|
|||||||
{
|
{
|
||||||
EXPECT_NE(kv.second.type(), Link::kUserClosure);
|
EXPECT_NE(kv.second.type(), Link::kUserClosure);
|
||||||
}
|
}
|
||||||
// Graph re-optimized without the rejected link -> mapCorrection identity.
|
// Graph re-optimized without the rejected link -> mapCorrection collapses
|
||||||
EXPECT_TRUE(rtabmap_->getMapCorrection().isIdentity());
|
// back toward identity. The rejected loop disagreed with the odom chain
|
||||||
|
// by 1 m, so a 1 cm residual is 99% undone. (g2o / GTSAM hit zero; TORO
|
||||||
|
// is gradient-descent so its floor is non-zero - around 6.5 mm at the
|
||||||
|
// default 100 iterations with epsilon 1e-10. Transform::isIdentity() is
|
||||||
|
// bit-exact, so we check the norm instead.)
|
||||||
|
const Transform mc = rtabmap_->getMapCorrection();
|
||||||
|
EXPECT_LT(mc.getNorm(), 1e-2f) << "post-reject correction: " << mc.prettyPrint();
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(RtabmapFixture, RejectLastLoopClosureIsNoOpWhenNoLoopClosureExists)
|
TEST_F(RtabmapFixture, RejectLastLoopClosureIsNoOpWhenNoLoopClosureExists)
|
||||||
@@ -1712,7 +1726,10 @@ TEST_F(RtabmapFixture, FollowLongPathWithIntermediateNodesRetrievesRealLtmNodes)
|
|||||||
for(const auto & kv : rtabmap_->getPath())
|
for(const auto & kv : rtabmap_->getPath())
|
||||||
{
|
{
|
||||||
const Signature * s = rtabmap_->getMemory()->getSignature(kv.first);
|
const Signature * s = rtabmap_->getMemory()->getSignature(kv.first);
|
||||||
if(s) EXPECT_NE(s->getWeight(), -1) << "intermediate id=" << kv.first << " on path";
|
if(s)
|
||||||
|
{
|
||||||
|
EXPECT_NE(s->getWeight(), -1) << "intermediate id=" << kv.first << " on path";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk back along the path. Path-follow advances and eventually reaches N1.
|
// Walk back along the path. Path-follow advances and eventually reaches N1.
|
||||||
@@ -2918,8 +2935,12 @@ TEST(RtabmapTest, GlobalBundleAdjustmentRefinesPosesOnSynthScene)
|
|||||||
/*align2D=*/false);
|
/*align2D=*/false);
|
||||||
return std::make_pair(t_rmse, r_rmse);
|
return std::make_pair(t_rmse, r_rmse);
|
||||||
};
|
};
|
||||||
const auto [tRmseBefore, rRmseBefore] = rmse(before);
|
const auto rmseBefore = rmse(before);
|
||||||
const auto [tRmseAfter, rRmseAfter] = rmse(after);
|
const auto rmseAfter = rmse(after);
|
||||||
|
const float tRmseBefore = rmseBefore.first;
|
||||||
|
const float rRmseBefore = rmseBefore.second;
|
||||||
|
const float tRmseAfter = rmseAfter.first;
|
||||||
|
const float rRmseAfter = rmseAfter.second;
|
||||||
// BA must reduce both translational and rotational RMSE toward GT, with
|
// BA must reduce both translational and rotational RMSE toward GT, with
|
||||||
// the residual bounded by the measurement noise floor.
|
// the residual bounded by the measurement noise floor.
|
||||||
EXPECT_LT(tRmseAfter, tRmseBefore) << "BA must reduce translational RMSE";
|
EXPECT_LT(tRmseAfter, tRmseBefore) << "BA must reduce translational RMSE";
|
||||||
@@ -3214,7 +3235,22 @@ TEST(RtabmapTest, LandmarkObservationsAcrossFramesShareSameLandmarkPose)
|
|||||||
// Two frames both observe landmark id=42 at the same world location.
|
// Two frames both observe landmark id=42 at the same world location.
|
||||||
// Memory stores the landmark once (key=-42 in the graph) and links both
|
// Memory stores the landmark once (key=-42 in the graph) and links both
|
||||||
// frames to it.
|
// frames to it.
|
||||||
|
//
|
||||||
|
// The default optimizer is built-dependent: GTSAM and g2o include the
|
||||||
|
// landmark as a graph variable; TORO ignores landmark constraints and
|
||||||
|
// won't expose -kLm in the optimized poses. Force a backend that
|
||||||
|
// supports landmarks; skip if none is available in this build.
|
||||||
|
int optimizerStrategy = -1;
|
||||||
|
if(Optimizer::isAvailable(Optimizer::kTypeGTSAM)) optimizerStrategy = Optimizer::kTypeGTSAM;
|
||||||
|
else if(Optimizer::isAvailable(Optimizer::kTypeG2O)) optimizerStrategy = Optimizer::kTypeG2O;
|
||||||
|
if(optimizerStrategy < 0)
|
||||||
|
{
|
||||||
|
GTEST_SKIP() << "neither GTSAM nor g2o is available; the default optimizer "
|
||||||
|
"(TORO/Ceres) does not include landmarks in the optimized graph";
|
||||||
|
}
|
||||||
ParametersMap params = defaultRtabmapParams();
|
ParametersMap params = defaultRtabmapParams();
|
||||||
|
params[Parameters::kOptimizerStrategy()] = uNumber2Str(optimizerStrategy);
|
||||||
|
params[Parameters::kOptimizerLandmarksIgnored()] = "false";
|
||||||
Rtabmap rtabmap;
|
Rtabmap rtabmap;
|
||||||
rtabmap.init(params);
|
rtabmap.init(params);
|
||||||
const cv::Mat cov = cv::Mat::eye(6, 6, CV_64FC1) * 0.01;
|
const cv::Mat cov = cv::Mat::eye(6, 6, CV_64FC1) * 0.01;
|
||||||
@@ -3657,8 +3693,12 @@ TEST(RtabmapTest, AggressiveLoopThresholdAcceptsBelowPrimaryThreshold)
|
|||||||
return std::make_pair(id, high);
|
return std::make_pair(id, high);
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto [idAgg, highAgg] = runOnce(kAggressiveThr);
|
const auto resultAgg = runOnce(kAggressiveThr);
|
||||||
const auto [idPrim, highPrim] = runOnce(kLoopThr); // aggressive disabled (= primary)
|
const auto resultPrim = runOnce(kLoopThr); // aggressive disabled (= primary)
|
||||||
|
const int idAgg = resultAgg.first;
|
||||||
|
const float highAgg = resultAgg.second;
|
||||||
|
const int idPrim = resultPrim.first;
|
||||||
|
const float highPrim = resultPrim.second;
|
||||||
|
|
||||||
// Sanity: both invocations see the same Bayes peak (deterministic data).
|
// Sanity: both invocations see the same Bayes peak (deterministic data).
|
||||||
EXPECT_NEAR(highAgg, highPrim, 1e-3);
|
EXPECT_NEAR(highAgg, highPrim, 1e-3);
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ TEST(Util3dMotionEstimationTest, EstimateMotion3DTo2DWithNoise) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(Util3dMotionEstimationTest, EstimateMotion3DTo2DMultiCamBasic) {
|
TEST(Util3dMotionEstimationTest, EstimateMotion3DTo2DMultiCamBasic) {
|
||||||
|
// OpenGV's RANSAC RNG defaults to a wall-clock seed, which makes the
|
||||||
|
// covariance / inlier outputs jitter across runs. Pin it for the test.
|
||||||
|
util3d::setRansacDeterministicSeed(true);
|
||||||
|
|
||||||
// Two triangles in front of the camera at two different depths, centered with the middle of the image frame
|
// Two triangles in front of the camera at two different depths, centered with the middle of the image frame
|
||||||
std::map<int, cv::Point3f> words3A = {
|
std::map<int, cv::Point3f> words3A = {
|
||||||
|
|||||||
Reference in New Issue
Block a user