mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-04 00:57:46 +08:00
making some tests less flaky
This commit is contained in:
@@ -74,6 +74,34 @@ namespace util3d
|
||||
*
|
||||
* @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(
|
||||
const std::map<int, cv::Point3f> & words3A,
|
||||
const std::map<int, cv::KeyPoint> & words2B,
|
||||
|
||||
@@ -56,6 +56,24 @@ namespace rtabmap
|
||||
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(
|
||||
const std::map<int, cv::Point3f> & words3A,
|
||||
const std::map<int, cv::KeyPoint> & words2B,
|
||||
@@ -517,6 +535,18 @@ Transform estimateMotion3DTo2D(
|
||||
std::shared_ptr<opengv::sac_problems::absolute_pose::MultiNoncentralAbsolutePoseSacProblem> absposeproblem_ptr(
|
||||
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.threshold_ = 1.0 - cos(atan(reprojError/cameraModels[0].fx()));
|
||||
ransac.max_iterations_ = iterations;
|
||||
@@ -572,6 +602,14 @@ Transform estimateMotion3DTo2D(
|
||||
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));
|
||||
|
||||
// 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.threshold_ = 1.0 - cos(atan(reprojError/cameraModels[0].fx()));
|
||||
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::kVisBundleAdjustment()] = "0";
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -134,15 +145,6 @@ static Transform computeRegistration(
|
||||
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).
|
||||
// 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.
|
||||
|
||||
@@ -1017,6 +1017,14 @@ TEST_F(RtabmapFixture, RejectLastLoopClosureRemovesLinkAndResetsHypothesis)
|
||||
// so mapCorrection collapses back to identity.
|
||||
ParametersMap params = defaultRtabmapParams();
|
||||
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);
|
||||
process();
|
||||
const int N1 = rtabmap_->getLastLocationId();
|
||||
@@ -1039,8 +1047,14 @@ TEST_F(RtabmapFixture, RejectLastLoopClosureRemovesLinkAndResetsHypothesis)
|
||||
{
|
||||
EXPECT_NE(kv.second.type(), Link::kUserClosure);
|
||||
}
|
||||
// Graph re-optimized without the rejected link -> mapCorrection identity.
|
||||
EXPECT_TRUE(rtabmap_->getMapCorrection().isIdentity());
|
||||
// Graph re-optimized without the rejected link -> mapCorrection collapses
|
||||
// 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)
|
||||
@@ -1712,7 +1726,10 @@ TEST_F(RtabmapFixture, FollowLongPathWithIntermediateNodesRetrievesRealLtmNodes)
|
||||
for(const auto & kv : rtabmap_->getPath())
|
||||
{
|
||||
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.
|
||||
@@ -2918,8 +2935,12 @@ TEST(RtabmapTest, GlobalBundleAdjustmentRefinesPosesOnSynthScene)
|
||||
/*align2D=*/false);
|
||||
return std::make_pair(t_rmse, r_rmse);
|
||||
};
|
||||
const auto [tRmseBefore, rRmseBefore] = rmse(before);
|
||||
const auto [tRmseAfter, rRmseAfter] = rmse(after);
|
||||
const auto rmseBefore = rmse(before);
|
||||
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
|
||||
// the residual bounded by the measurement noise floor.
|
||||
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.
|
||||
// Memory stores the landmark once (key=-42 in the graph) and links both
|
||||
// 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();
|
||||
params[Parameters::kOptimizerStrategy()] = uNumber2Str(optimizerStrategy);
|
||||
params[Parameters::kOptimizerLandmarksIgnored()] = "false";
|
||||
Rtabmap rtabmap;
|
||||
rtabmap.init(params);
|
||||
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);
|
||||
};
|
||||
|
||||
const auto [idAgg, highAgg] = runOnce(kAggressiveThr);
|
||||
const auto [idPrim, highPrim] = runOnce(kLoopThr); // aggressive disabled (= primary)
|
||||
const auto resultAgg = runOnce(kAggressiveThr);
|
||||
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).
|
||||
EXPECT_NEAR(highAgg, highPrim, 1e-3);
|
||||
|
||||
@@ -243,6 +243,9 @@ TEST(Util3dMotionEstimationTest, EstimateMotion3DTo2DWithNoise) {
|
||||
}
|
||||
|
||||
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
|
||||
std::map<int, cv::Point3f> words3A = {
|
||||
|
||||
Reference in New Issue
Block a user