Skip to content

Commit c8cd60d

Browse files
committed
Expose more parameters for F2F algorithm
1 parent f6fd235 commit c8cd60d

File tree

3 files changed

+79
-47
lines changed

3 files changed

+79
-47
lines changed

module/include/mola_sm_loop_closure/FrameToFrameLoopClosure.h

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ class FrameToFrameLoopClosure : public mola::LoopClosureInterface
7979
size_t min_frames_between_lc = 50; // minimum frame separation
8080
size_t max_lc_optimization_rounds = 5; // maximum LC+optimization rounds to run
8181

82+
/** Number of accepted LCs between intermediate graph optimizations.
83+
* Within each round, after this many accepted loop closures the graph
84+
* is re-optimized so that later (larger-gap) candidates benefit from
85+
* the corrections of earlier (smaller-gap) ones.
86+
* Set to 0 to disable intermediate optimizations (optimize only at
87+
* end of each round, original behavior).
88+
*/
89+
size_t lc_optimize_every_n = 5;
90+
8291
/** Loop closure candidate selection strategy */
8392
enum class CandidateSelectionStrategy : uint8_t
8493
{
@@ -169,19 +178,19 @@ class FrameToFrameLoopClosure : public mola::LoopClosureInterface
169178
std::string debug_files_prefix = "f2f_lc_";
170179

171180
// 3D scene visualization output
172-
bool save_3d_scene_files = false;
173-
bool save_3d_scene_files_per_iteration = false;
174-
float scene_path_line_width = 2.0f;
175-
float scene_lc_line_width = 4.0f;
176-
float scene_path_color_r = 0.0f;
177-
float scene_path_color_g = 0.0f;
178-
float scene_path_color_b = 1.0f; // blue
179-
float scene_path_color_a = 0.7f;
180-
float scene_lc_color_r = 1.0f; // red
181-
float scene_lc_color_g = 0.0f;
182-
float scene_lc_color_b = 0.0f;
183-
float scene_lc_color_a = 0.8f;
184-
float scene_keyframe_point_size = 7.0f;
181+
bool save_3d_scene_files = false;
182+
bool save_3d_scene_files_per_iteration = false;
183+
float scene_path_line_width = 2.0f;
184+
float scene_lc_line_width = 4.0f;
185+
float scene_path_color_r = 0.0f;
186+
float scene_path_color_g = 0.0f;
187+
float scene_path_color_b = 1.0f; // blue
188+
float scene_path_color_a = 0.7f;
189+
float scene_lc_color_r = 1.0f; // red
190+
float scene_lc_color_g = 0.0f;
191+
float scene_lc_color_b = 0.0f;
192+
float scene_lc_color_a = 0.8f;
193+
float scene_keyframe_point_size = 7.0f;
185194
};
186195

187196
Parameters params_;

module/src/FrameToFrameLoopClosure.cpp

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ void FrameToFrameLoopClosure::initialize(const mrpt::containers::yaml& c)
194194
YAML_LOAD_OPT(params_, max_lc_candidates, size_t);
195195
YAML_LOAD_OPT(params_, min_frames_between_lc, size_t);
196196
YAML_LOAD_OPT(params_, max_lc_optimization_rounds, size_t);
197+
YAML_LOAD_OPT(params_, lc_optimize_every_n, size_t);
197198

198199
if (params_.min_frames_between_lc == 0)
199200
{
@@ -357,21 +358,25 @@ void FrameToFrameLoopClosure::process(mrpt::maps::CSimpleMap& sm) // NOLINT
357358

358359
MRPT_LOG_INFO_STREAM("Found " << candidates.size() << " loop closure candidates");
359360

360-
// Sort candidates by frame locality to maximize point cloud cache hits:
361+
// Sort candidates by ascending topological gap (frame index separation)
362+
// so that inner (smaller) loops are closed first, improving the graph
363+
// before attempting larger loops:
361364
std::sort(
362365
candidates.begin(), candidates.end(),
363366
[](const LoopCandidate& a, const LoopCandidate& b)
364367
{
365-
const auto minA = std::min(a.frame_i, a.frame_j);
366-
const auto minB = std::min(b.frame_i, b.frame_j);
367-
if (minA != minB)
368+
const auto gapA = a.frame_j - a.frame_i;
369+
const auto gapB = b.frame_j - b.frame_i;
370+
if (gapA != gapB)
368371
{
369-
return minA < minB;
372+
return gapA < gapB;
370373
}
371-
return std::max(a.frame_i, a.frame_j) < std::max(b.frame_i, b.frame_j);
374+
// Tie-break: earlier frame first (cache locality)
375+
return std::min(a.frame_i, a.frame_j) < std::min(b.frame_i, b.frame_j);
372376
});
373377

374-
const auto frameGroup = static_cast<double>(params_.min_frames_between_lc);
378+
const auto frameGroup = static_cast<double>(params_.min_frames_between_lc);
379+
size_t acceptedSinceLastOpt = 0;
375380

376381
for (const auto& lc : candidates)
377382
{
@@ -397,6 +402,19 @@ void FrameToFrameLoopClosure::process(mrpt::maps::CSimpleMap& sm) // NOLINT
397402
{
398403
anyGraphChange = true;
399404
accepted_lcs++;
405+
acceptedSinceLastOpt++;
406+
407+
// Intermediate optimization: re-optimize after every N accepted LCs
408+
// so that later (larger-gap) candidates benefit from corrected poses.
409+
if (params_.lc_optimize_every_n > 0 &&
410+
acceptedSinceLastOpt >= params_.lc_optimize_every_n)
411+
{
412+
MRPT_LOG_INFO_STREAM(
413+
"Intermediate optimization after " << acceptedSinceLastOpt
414+
<< " accepted LCs");
415+
optimize_graph();
416+
acceptedSinceLastOpt = 0;
417+
}
400418
}
401419
}
402420

@@ -405,8 +423,9 @@ void FrameToFrameLoopClosure::process(mrpt::maps::CSimpleMap& sm) // NOLINT
405423
break; // No new candidates
406424
}
407425

408-
if (anyGraphChange)
426+
if (anyGraphChange && acceptedSinceLastOpt > 0)
409427
{
428+
// Final optimization for remaining accepted LCs in this round
410429
const double largestDelta = optimize_graph();
411430

412431
if (params_.save_3d_scene_files && params_.save_3d_scene_files_per_iteration)
@@ -1130,21 +1149,28 @@ double FrameToFrameLoopClosure::optimize_graph()
11301149

11311150
// Log GNC weights for LC edges (for diagnostics)
11321151
const auto& gncWeights = gnc.getWeights();
1133-
size_t numLcOutliers = 0;
1152+
size_t numLcOutliers = 0;
1153+
size_t numLcInliers = 0;
11341154
for (size_t k = 0; k < static_cast<size_t>(gncWeights.size()); k++)
11351155
{
11361156
// Check if this factor is NOT a known inlier (i.e., it's an LC edge)
11371157
const bool isKnownInlier = std::binary_search(
11381158
state_.knownInlierFactorIndices.begin(), state_.knownInlierFactorIndices.end(), k);
1139-
if (!isKnownInlier && gncWeights[k] < 0.5)
1159+
if (!isKnownInlier)
11401160
{
1141-
numLcOutliers++;
1161+
if (gncWeights[k] < 0.5)
1162+
{
1163+
numLcOutliers++;
1164+
}
1165+
else
1166+
{
1167+
numLcInliers++;
1168+
}
11421169
}
11431170
}
1144-
if (numLcOutliers > 0)
1145-
{
1146-
MRPT_LOG_INFO_STREAM("GNC rejected " << numLcOutliers << " LC edge(s) as outliers");
1147-
}
1171+
MRPT_LOG_INFO_STREAM(
1172+
"GNC result: " << numLcInliers << " LC inlier(s), " << numLcOutliers
1173+
<< " LC outlier(s) rejected");
11481174

11491175
// Compute largest pose change
11501176
double largestDelta = 0.0;
@@ -1318,11 +1344,10 @@ void FrameToFrameLoopClosure::save_3d_scene_files(const std::string& suffix) con
13181344
{
13191345
auto lines = mrpt::opengl::CSetOfLines::Create();
13201346
lines->setLineWidth(params_.scene_path_line_width);
1321-
lines->setColor_u8(
1322-
mrpt::img::TColorf(
1323-
params_.scene_path_color_r, params_.scene_path_color_g, params_.scene_path_color_b,
1324-
params_.scene_path_color_a * 255)
1325-
.asTColor());
1347+
lines->setColor_u8(mrpt::img::TColorf(
1348+
params_.scene_path_color_r, params_.scene_path_color_g,
1349+
params_.scene_path_color_b, params_.scene_path_color_a * 255)
1350+
.asTColor());
13261351

13271352
for (size_t i = 1; i < sm.size(); i++)
13281353
{
@@ -1348,11 +1373,10 @@ void FrameToFrameLoopClosure::save_3d_scene_files(const std::string& suffix) con
13481373
{
13491374
auto pts = mrpt::opengl::CPointCloud::Create();
13501375
pts->setPointSize(params_.scene_keyframe_point_size);
1351-
pts->setColor_u8(
1352-
mrpt::img::TColorf(
1353-
params_.scene_path_color_r, params_.scene_path_color_g, params_.scene_path_color_b,
1354-
params_.scene_path_color_a)
1355-
.asTColor());
1376+
pts->setColor_u8(mrpt::img::TColorf(
1377+
params_.scene_path_color_r, params_.scene_path_color_g,
1378+
params_.scene_path_color_b, params_.scene_path_color_a)
1379+
.asTColor());
13561380

13571381
for (size_t i = 0; i < sm.size(); i++)
13581382
{
@@ -1377,11 +1401,10 @@ void FrameToFrameLoopClosure::save_3d_scene_files(const std::string& suffix) con
13771401
{
13781402
auto lines = mrpt::opengl::CSetOfLines::Create();
13791403
lines->setLineWidth(params_.scene_lc_line_width);
1380-
lines->setColor_u8(
1381-
mrpt::img::TColorf(
1382-
params_.scene_lc_color_r, params_.scene_lc_color_g, params_.scene_lc_color_b,
1383-
params_.scene_lc_color_a)
1384-
.asTColor());
1404+
lines->setColor_u8(mrpt::img::TColorf(
1405+
params_.scene_lc_color_r, params_.scene_lc_color_g,
1406+
params_.scene_lc_color_b, params_.scene_lc_color_a)
1407+
.asTColor());
13851408

13861409
for (const auto& [fi, fj] : accepted_lc_edges_)
13871410
{

pipelines/loop-closure-f2f-lidar3d-gicp.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
# Licensed under the GNU GPL v3.
66
# -----------------------------------------------------------------------------
77

8-
# Configuration file for Frame-to-Frame Loop Closure
9-
# This implements a simpler alternative to submap-based loop closure:
8+
# Configuration file for Frame-to-Frame Loop Closure (mola::FrameToFrameLoopClosure)
109

1110
params:
1211
# ===== GNSS Optimization Parameters =====
@@ -22,6 +21,7 @@ params:
2221
max_lc_candidates: "${MAX_LC_CANDIDATES|20}" # Maximum candidates per iteration
2322
min_frames_between_lc: "${MIN_FRAMES_BETWEEN_LC|20}" # Minimum frame index separation
2423
max_lc_optimization_rounds: "${MAX_LC_OPTIMIZATION_ROUNDS|5}" # Maximum LC+optimization rounds to run
24+
lc_optimize_every_n: "${LC_OPTIMIZE_EVERY_N|5}" # Re-optimize after N accepted LCs (0=disable, closes inner loops first)
2525

2626
lc_candidate_strategy: "${LC_SELECTION_METHOD|DISTANCE_STRATIFIED}" # or PROXIMITY_ONLY, MULTI_OBJECTIVE
2727
lc_verbose_candidate_selection: "${LC_SELECTION_VERBOSE|0}"
@@ -44,8 +44,8 @@ params:
4444
threshold_sigma_final: "${LC_ICP_FINAL_SIGMA|0.05}"
4545

4646
# ===== Odometry Edge Parameters =====
47-
input_odometry_noise_xyz: 0.04 # [m] Odometry translational noise (per meter of travel if scaled)
48-
input_odometry_noise_ang: 0.10 # [deg] Odometry angular noise (per meter of travel if scaled)
47+
input_odometry_noise_xyz: "${INPUT_ODOMETRY_NOISE_XYZ_PER_M|0.04}" # [m] Odometry translational noise (per meter of travel if scaled)
48+
input_odometry_noise_ang: "${INPUT_ODOMETRY_NOISE_ANG_DEG_PER_M|0.05}" # [deg] Odometry angular noise (per meter of travel if scaled)
4949
scale_odometry_noise_by_distance: true # Scale noise proportionally to inter-frame distance
5050

5151
# ===== Optimization Control =====

0 commit comments

Comments
 (0)