mirror of
https://github.com/opencv/opencv.git
synced 2026-07-30 15:53:03 +04:00
Merge pull request #27423 from miguel1099:feat-#25150-MST_init_poseGraph
Feat #25150: Pose Graph MST initialization #27423 Implements an MST-based initialisation for pose graphs, as proposed in issue #25150. Both Prim’s and Kruskal’s algorithms were added to the 3D module. These receive a vector of node IDs and a vector of edges (each with source and target IDs and a weight), and return a vector with the resulting edges. These MST implementations treat edges as undirected internally, meaning users only need to provide one direction (A→B or B→A), and duplicates are handled automatically. Additionally, a new pose graph initialisation method using MST (Prim) was implemented. It constructs the MST over the pose graph, then traverses it to reconstruct node poses. With this, users can call `poseGraph->initializePosesWithMST()` to create an initial solution for the pose graph problem. A set of test cases validating the implementation was also included. #### Notes - The edge weight used in the MST for pose graphs is calculated as: `weight = || translation || + λ * rotation_angle`, where λ = 0.485 was determined empirically based on optimiser performance; - Validated on [Sphere-a](https://lucacarlone.mit.edu/datasets/) pose graph, showing similar convergence behaviour with or without MST initialisation; - Alternative weight formulas, such as the Mahalanobis distance formula, were also tested, but the current formula yielded better results. #### Future Work - Extend testing to more diverse pose graphs (currently limited to [Sphere-a](https://github.com/opencv/opencv_extra/blob/5.x/testdata/cv/rgbd/sphere_bignoise_vertex3.g2o) in opencv_extra) - Explore adaptive tuning of the λ parameter for broader applicability. Co-authored-by: @miguel1099 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
@@ -155,6 +155,119 @@ TEST(PoseGraph, sphereG2O)
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PoseGraphMST, optimization)
|
||||
{
|
||||
applyTestTag(CV_TEST_TAG_LONG, CV_TEST_TAG_DEBUG_VERYLONG);
|
||||
|
||||
// The dataset was taken from here: https://lucacarlone.mit.edu/datasets/
|
||||
// Connected paper:
|
||||
// L.Carlone, R.Tron, K.Daniilidis, and F.Dellaert.
|
||||
// Initialization Techniques for 3D SLAM : a Survey on Rotation Estimation and its Use in Pose Graph Optimization.
|
||||
// In IEEE Intl.Conf.on Robotics and Automation(ICRA), pages 4597 - 4604, 2015.
|
||||
|
||||
std::string filename = cvtest::TS::ptr()->get_data_path() + "/cv/rgbd/sphere_bignoise_vertex3.g2o";
|
||||
|
||||
Ptr<detail::PoseGraph> pgOptimizerOnly = readG2OFile(filename);
|
||||
Ptr<detail::PoseGraph> pgWithMSTAndOptimizer = readG2OFile(filename);
|
||||
Ptr<detail::PoseGraph> init = readG2OFile(filename);
|
||||
|
||||
double lambda = 0.485;
|
||||
pgWithMSTAndOptimizer->initializePosesWithMST(lambda);
|
||||
|
||||
// You may change logging level to view detailed optimization report
|
||||
// For example, set env. variable like this: OPENCV_LOG_LEVEL=INFO
|
||||
|
||||
// geoScale=1 is experimental, not guaranteed to work on other problems
|
||||
// the rest are default params
|
||||
pgOptimizerOnly->createOptimizer(LevMarq::Settings().setGeoScale(1.0)
|
||||
.setMaxIterations(100)
|
||||
.setCheckRelEnergyChange(true)
|
||||
.setRelEnergyDeltaTolerance(1e-6)
|
||||
.setGeodesic(true));
|
||||
pgWithMSTAndOptimizer->createOptimizer(LevMarq::Settings().setGeoScale(1.0)
|
||||
.setMaxIterations(100)
|
||||
.setCheckRelEnergyChange(true)
|
||||
.setRelEnergyDeltaTolerance(1e-6)
|
||||
.setGeodesic(true));
|
||||
|
||||
auto r1 = pgWithMSTAndOptimizer->optimize();
|
||||
auto r2 = pgOptimizerOnly->optimize();
|
||||
|
||||
EXPECT_TRUE(r1.found);
|
||||
EXPECT_TRUE(r2.found);
|
||||
EXPECT_LE(r2.energy, 1.47723e+06);
|
||||
// Allow small tolerance due to optimization differences; final energy/iterations are effectively the same
|
||||
EXPECT_LE(std::abs(r1.energy - r2.energy), 1e-2);
|
||||
ASSERT_LE(std::abs(r1.iters - r2.iters), 1);
|
||||
|
||||
// Add the "--test_debug" to arguments to see resulting pose graph nodes positions
|
||||
if (cvtest::debugLevel > 0)
|
||||
{
|
||||
// Note:
|
||||
// A custom .obj writer is used here instead of cv::saveMesh because saveMesh expects faces
|
||||
// (i.e., polygons with 3 or more vertices) and writes them using the "f i j k..." syntax in
|
||||
// the .obj file. Since pose graphs consist of edges rather than polygonal faces, we represent
|
||||
// them using line segments ("l i j").
|
||||
// As saveMesh does not support writing "l" lines, it is not suitable in this context.
|
||||
|
||||
std::string fname;
|
||||
std::fstream of;
|
||||
std::vector<size_t> ids;
|
||||
size_t esz;
|
||||
|
||||
// Write OBJ for MST-initialized pose graph with optimizer
|
||||
fname = "pg_with_mst_and_optimizer.obj";
|
||||
of.open(fname, std::fstream::out);
|
||||
ids = pgWithMSTAndOptimizer->getNodesIds();
|
||||
for (const size_t& id : ids)
|
||||
{
|
||||
Point3d d = pgWithMSTAndOptimizer->getNodePose(id).translation();
|
||||
of << "v " << d.x << " " << d.y << " " << d.z << std::endl;
|
||||
}
|
||||
esz = pgWithMSTAndOptimizer->getNumEdges();
|
||||
for (size_t i = 0; i < esz; i++)
|
||||
{
|
||||
size_t sid = pgWithMSTAndOptimizer->getEdgeStart(i), tid = pgWithMSTAndOptimizer->getEdgeEnd(i);
|
||||
of << "l " << sid + 1 << " " << tid + 1 << std::endl;
|
||||
}
|
||||
of.close();
|
||||
|
||||
// Write OBJ for optimizer-only pose graph
|
||||
fname = "pg_optimizer_only.obj";
|
||||
of.open(fname, std::fstream::out);
|
||||
ids = pgOptimizerOnly->getNodesIds();
|
||||
for (const size_t& id : ids)
|
||||
{
|
||||
Point3d d = pgOptimizerOnly->getNodePose(id).translation();
|
||||
of << "v " << d.x << " " << d.y << " " << d.z << std::endl;
|
||||
}
|
||||
esz = pgOptimizerOnly->getNumEdges();
|
||||
for (size_t i = 0; i < esz; i++)
|
||||
{
|
||||
size_t sid = pgOptimizerOnly->getEdgeStart(i), tid = pgOptimizerOnly->getEdgeEnd(i);
|
||||
of << "l " << sid + 1 << " " << tid + 1 << std::endl;
|
||||
}
|
||||
of.close();
|
||||
|
||||
// Write OBJ for initial pose graph
|
||||
fname = "pg_init.obj";
|
||||
of.open(fname, std::fstream::out);
|
||||
ids = init->getNodesIds();
|
||||
for (const size_t& id : ids)
|
||||
{
|
||||
Point3d d = init->getNodePose(id).translation();
|
||||
of << "v " << d.x << " " << d.y << " " << d.z << std::endl;
|
||||
}
|
||||
esz = init->getNumEdges();
|
||||
for (size_t i = 0; i < esz; i++)
|
||||
{
|
||||
size_t sid = init->getEdgeStart(i), tid = init->getEdgeEnd(i);
|
||||
of << "l " << sid + 1 << " " << tid + 1 << std::endl;
|
||||
}
|
||||
of.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
// Wireframe meshes for debugging visualization purposes
|
||||
@@ -396,6 +509,11 @@ TEST(PoseGraph, sphereG2O)
|
||||
throw SkipTestException("Build with Eigen required for pose graph optimization");
|
||||
}
|
||||
|
||||
TEST(PoseGraphMST, optimization)
|
||||
{
|
||||
throw SkipTestException("Build with Eigen required for pose graph optimization");
|
||||
}
|
||||
|
||||
TEST(PoseGraph, simple)
|
||||
{
|
||||
throw SkipTestException("Build with Eigen required for pose graph optimization");
|
||||
|
||||
Reference in New Issue
Block a user